Low watch CVSS 5.3 chrome Info Disclosure 🔧 Commit mapped

Overview

Low
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactObservable discrepancy in Glic
DescriptionObservable discrepancy in Glic
ComponentGlic
Sub-componentBlink
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker506539337
Fix commit6937cb76b634 (chromium/src) +169/-56
CWECWE-203
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-26 (per vendor advisory)
Fix landed2026-07-27 (public git)
Disclosed2026-08-25
Patch gap29 days — fix public in source before the advisory

Background

`requestAnimationFrame` (rAF)
the web-exposed API that registers a callback to run before the next paint, backed in Blink by FrameRequestCallbackCollection.
`FrameRequestCallbackCollection`
holds registered frame callbacks in frame_callbacks_, assigns each an id from next_callback_id_, and exposes RegisterFrameCallback/CancelFrameCallback/ExecuteFrameCallbacks.
Internal rAF users
engine components (e.g. DocumentMarkerController’s Glic marker animation, HTMLMarqueeElement, PaintTiming) call Document::RequestAnimationFrame internally, sharing the same collection as web requestAnimationFrame.
`FrameCallbackType`
new enum (kWebExposed, kInternal) introduced by the patch to distinguish web-page callbacks from engine-internal callbacks.

Root Cause Analysis

Before the patch, both web requestAnimationFrame and engine-internal rAF registrations funneled into a single frame_callbacks_ list with ids drawn from one shared counter next_callback_id_, and CancelFrameCallback searched that single list by id. This conflated two trust domains: the id returned to a web page depended on how many internal callbacks had been registered, and a page’s cancelAnimationFrame(id) could match and cancel an internal callback that happened to share the id. That created an observable discrepancy — a web page could infer the presence/activity of internal animations (such as the Glic marker animation registered from DocumentMarkerController) from id allocation or cancellation side effects.

The fix adds a FrameCallbackType parameter threaded through Document, ScriptedAnimationController, WorkerAnimationFrameProvider, and FrameRequestCallbackCollection, giving internal callbacks their own list internal_frame_callbacks_ and their own id counter next_internal_callback_id_, and making RegisterFrameCallback/CancelFrameCallback operate only on the list matching the caller’s type. Web entry points (LocalDOMWindow) pass kWebExposed while internal callers (DocumentMarkerController, HTMLMarqueeElement, PaintTiming, etc.) pass kInternal, so the two pools no longer share ids or cancellation scope. ExecuteFrameCallbacks now snapshots and runs both lists via the shared ExecuteFrameCallbacksImpl so behavior for scheduling is preserved.

Key insight
Web-exposed and engine-internal animation-frame callbacks shared one id space and one list, letting web script observe or cancel internal callbacks; the fix partitions them into separate id counters and separate lists keyed by FrameCallbackType.

Attack Path

  1. Trigger an internal rAF user The attacker page induces an engine feature that schedules internal rAF callbacks (e.g. conditions that start the Glic marker animation in DocumentMarkerController, or a marquee).
  2. Register/observe web callbacks The page calls requestAnimationFrame and inspects the returned ids, which pre-patch are drawn from the same next_callback_id_ counter shared with internal registrations.
  3. Detect the discrepancy By correlating gaps/values in the returned ids (or by calling cancelAnimationFrame on candidate ids and observing effects) the page infers whether internal callbacks are registered, leaking otherwise-unobservable engine state (the reported observable discrepancy in Glic).
  4. Cross-domain cancellation (inferred) Because CancelFrameCallback scanned the single shared list, a web cancelAnimationFrame(id) could cancel an internal callback with a colliding id, perturbing internal animation state.

Impact Assessment

A web page in the renderer can observe an information-leaking discrepancy in how animation-frame callback ids are allocated (and potentially cancel internal callbacks), allowing it to infer the presence or activity of internal engine features such as the Glic marker animation without user consent. The precondition is only the ability to run script and call requestAnimationFrame/cancelAnimationFrame. The advisory classifies this as an observable discrepancy / information disclosure in Glic; no exploited-in-the-wild status or higher escalation is stated.

Changed Functions

FunctionChangeNotes
RegisterFrameCallback
third_party/blink/renderer/core/dom/frame_request_callback_collection.cc
modified
if
third_party/blink/renderer/core/dom/frame_request_callback_collection.cc
modified
for
third_party/blink/renderer/core/dom/frame_request_callback_collection.cc
modified
CORE_EXPORT
third_party/blink/renderer/core/dom/frame_request_callback_collection.h
modified
RegisterFrameCallback
third_party/blink/renderer/core/dom/scripted_animation_controller.cc
modified
if
third_party/blink/renderer/core/html/html_marquee_element.cc
modified
if
third_party/blink/renderer/core/paint/timing/paint_timing.cc
modified

Files Changed

  • third_party/blink/renderer/core/editing/markers/document_marker_controller.cc
  • third_party/blink/renderer/core/dom/scripted_animation_controller.cc
  • third_party/blink/renderer/modules/xr/xr_frame_provider.cc
  • third_party/blink/renderer/core/animation_frame/worker_animation_frame_provider.cc
  • third_party/blink/renderer/core/dom/frame_request_callback_collection.cc
  • third_party/blink/renderer/core/dom/frame_request_callback_collection.h
  • third_party/blink/renderer/modules/media_controls/elements/media_control_playback_speed_list_element.cc
  • third_party/blink/renderer/core/dom/document.h
  • third_party/blink/renderer/core/dom/scripted_animation_controller_test.cc
  • third_party/blink/renderer/core/paint/timing/paint_timing.cc
  • third_party/blink/renderer/core/html/html_marquee_element.cc
  • third_party/blink/renderer/core/dom/document.cc
  • third_party/blink/renderer/core/frame/document_loading_rendering_test.cc
  • third_party/blink/renderer/core/dom/scripted_animation_controller.h
  • third_party/blink/renderer/core/frame/local_dom_window.cc

Audit Directions

  • Shared id/handle spaces across trust boundaries
    Look for other collections where web-exposed handles and internal handles are drawn from one counter or one container, enabling id-based inference or cross-cancellation.
  • Internal reuse of web-exposed APIs
    Audit engine components that call web-facing scheduling primitives (RequestAnimationFrame, timers, IDs) directly, and verify their state is not observable or cancellable from script.
  • Cancellation by id without ownership check
    Review Cancel*/Remove* methods that match solely on a caller-supplied id to ensure they cannot reach objects registered by a different origin/subsystem.
From 6937cb76b634b8f44877a3e9443b30baddf28c5f Mon Sep 17 00:00:00 2001
From: William Liu <[email protected]>
Date: Mon, 27 Jul 2026 11:02:16 -0700
Subject: [PATCH] [blink] Distinguish between Web and internal requestAnimationFrame

Fixed: b:506539337
Change-Id: I891083a789417a1d9317b95223091d3c65f30bc1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8144040
Reviewed-by: Adithya Srinivasan <[email protected]>
Commit-Queue: William Liu <[email protected]>
Reviewed-by: Philip Rogers <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1668807}
---
diff --git a/third_party/blink/renderer/core/animation_frame/worker_animation_frame_provider.cc b/third_party/blink/renderer/core/animation_frame/worker_animation_frame_provider.cc
index 7a646f03..2a9b35f 100644
--- a/third_party/blink/renderer/core/animation_frame/worker_animation_frame_provider.cc
+++ b/third_party/blink/renderer/core/animation_frame/worker_animation_frame_provider.cc
@@ -32,13 +32,14 @@
   }
 
   FrameRequestCallbackCollection::CallbackId id =
-      callback_collection_.RegisterFrameCallback(callback);
+      callback_collection_.RegisterFrameCallback(
+          callback, FrameCallbackType::kWebExposed);
   begin_frame_provider_->RequestBeginFrame();
   return id;
 }
 
 void WorkerAnimationFrameProvider::CancelCallback(int id) {
-  callback_collection_.CancelFrameCallback(id);
+  callback_collection_.CancelFrameCallback(id, FrameCallbackType::kWebExposed);
 }
 
 void WorkerAnimationFrameProvider::BeginFrame(const viz::BeginFrameArgs& args) {
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index 460ac2eb..e3961a3 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -8849,12 +8849,13 @@
   return *scripted_animation_controller_;
 }
 
-int Document::RequestAnimationFrame(FrameCallback* callback) {
-  return scripted_animation_controller_->RegisterFrameCallback(callback);
+int Document::RequestAnimationFrame(FrameCallback* callback,
+                                    FrameCallbackType type) {
+  return scripted_animation_controller_->RegisterFrameCallback(callback, type);
 }
 
-void Document::CancelAnimationFrame(int id) {
-  scripted_animation_controller_->CancelFrameCallback(id);
+void Document::CancelAnimationFrame(int id, FrameCallbackType type) {
+  scripted_animation_controller_->CancelFrameCallback(id, type);
 }
 
 DocumentLoader* Document::Loader() const {
diff --git a/third_party/blink/renderer/core/dom/document.h b/third_party/blink/renderer/core/dom/document.h
index 6e735c85..a6c0c2e0 100644
--- a/third_party/blink/renderer/core/dom/document.h
+++ b/third_party/blink/renderer/core/dom/document.h
@@ -75,6 +75,7 @@
 #include "third_party/blink/renderer/core/dom/element.h"
 #include "third_party/blink/renderer/core/dom/events/event_path.h"
 #include "third_party/blink/renderer/core/dom/focus_params.h"
+#include "third_party/blink/renderer/core/dom/frame_request_callback_collection.h"
 #include "third_party/blink/renderer/core/dom/live_node_list_registry.h"
 #include "third_party/blink/renderer/core/dom/node_list_invalidation_type.h"
 #include "third_party/blink/renderer/core/dom/qualified_name.h"
@@ -1660,8 +1661,8 @@
            IsInOutermostMainFrame();
   }
 
-  int RequestAnimationFrame(FrameCallback*);
-  void CancelAnimationFrame(int id);
+  int RequestAnimationFrame(FrameCallback*, FrameCallbackType type);
+  void CancelAnimationFrame(int id, FrameCallbackType type);
 
   ScriptedAnimationController& GetScriptedAnimationController();
 
diff --git a/third_party/blink/renderer/core/dom/frame_request_callback_collection.cc b/third_party/blink/renderer/core/dom/frame_request_callback_collection.cc
index d20b374..1b6f2302 100644
--- a/third_party/blink/renderer/core/dom/frame_request_callback_collection.cc
+++ b/third_party/blink/renderer/core/dom/frame_request_callback_collection.cc
@@ -16,12 +16,18 @@
     : context_(context) {}
 
 FrameRequestCallbackCollection::CallbackId
-FrameRequestCallbackCollection::RegisterFrameCallback(FrameCallback* callback) {
-  FrameRequestCallbackCollection::CallbackId id = ++next_callback_id_;
+FrameRequestCallbackCollection::RegisterFrameCallback(FrameCallback* callback,
+                                                      FrameCallbackType type) {
+  FrameRequestCallbackCollection::CallbackId id =
+      (type == FrameCallbackType::kInternal) ? ++next_internal_callback_id_
+                                             : ++next_callback_id_;
   callback->SetIsCancelled(false);
   callback->SetId(id);
-  frame_callbacks_.push_back(callback);
-
+  if (type == FrameCallbackType::kInternal) {
+    internal_frame_callbacks_.push_back(callback);
+  } else {
+    frame_callbacks_.push_back(callback);
+  }
   DEVTOOLS_TIMELINE_TRACE_EVENT_INSTANT("RequestAnimationFrame",
                                         inspector_animation_frame_event::Data,
                                         context_, id);
@@ -30,19 +36,28 @@
   return id;
 }
 
-void FrameRequestCallbackCollection::CancelFrameCallback(CallbackId id) {
-  for (wtf_size_t i = 0; i < frame_callbacks_.size(); ++i) {
-    if (frame_callbacks_[i]->Id() == id) {
-      frame_callbacks_[i]->async_task_context()->Cancel();
+void FrameRequestCallbackCollection::CancelFrameCallback(
+    CallbackId id,
+    FrameCallbackType type) {
+  auto& callbacks = (type == FrameCallbackType::kInternal)
+                        ? internal_frame_callbacks_
+                        : frame_callbacks_;
+  auto& callbacks_to_invoke = (type == FrameCallbackType::kInternal)
+                                  ? internal_callbacks_to_invoke_
+                                  : callbacks_to_invoke_;
+
+  for (wtf_size_t i = 0; i < callbacks.size(); ++i) {
+    if (callbacks[i]->Id() == id) {
+      callbacks[i]->async_task_context()->Cancel();
       probe::BreakableLocation(context_, "cancelAnimationFrame");
-      frame_callbacks_.EraseAt(i);
+      callbacks.EraseAt(i);
       DEVTOOLS_TIMELINE_TRACE_EVENT_INSTANT(
           "CancelAnimationFrame", inspector_animation_frame_event::Data,
           context_.Get(), id);
       return;
     }
   }
-  for (const auto& callback : callbacks_to_invoke_) {
+  for (const auto& callback : callbacks_to_invoke) {
     if (callback->Id() == id) {
       callback->async_task_context()->Cancel();
       probe::BreakableLocation(context_, "cancelAnimationFrame");
@@ -64,19 +79,32 @@
   ExecutionContext::ScopedRequestAnimationFrameStatus scoped_raf_status(
       context_);
 
-  // First, generate a list of callbacks to consider.  Callbacks registered from
-  // this point on are considered only for the "next" frame, not this one.
+  // First, snapshot both lists. Callbacks registered from this point on are
+  // considered only for the "next" frame, not this one.
   DCHECK(callbacks_to_invoke_.empty());
+  DCHECK(internal_callbacks_to_invoke_.empty());
   swap(callbacks_to_invoke_, frame_callbacks_);
+  swap(internal_callbacks_to_invoke_, internal_frame_callbacks_);
 
-  for (const auto& callback : callbacks_to_invoke_) {
+  ExecuteFrameCallbacksImpl(callbacks_to_invoke_, high_res_now_ms,
+                            high_res_now_ms_legacy);
+  ExecuteFrameCallbacksImpl(internal_callbacks_to_invoke_, high_res_now_ms,
+                            high_res_now_ms_legacy);
+}
+
+void FrameRequestCallbackCollection::ExecuteFrameCallbacksImpl(
+    CallbackList& callbacks_to_invoke,
+    double high_res_now_ms,
+    double high_res_now_ms_legacy) {
+  for (const auto& callback : callbacks_to_invoke) {
     // When the ExecutionContext is destroyed (e.g. an iframe is detached),
     // there is no path to perform wrapper tracing for the callbacks. In such a
     // case, the callback functions may already have been collected by V8 GC.
     // Since it's possible that a callback function being invoked detaches an
     // iframe, we need to check the condition for each callback.
-    if (context_->IsContextDestroyed())
+    if (context_->IsContextDestroyed()) {
       break;
+    }
     if (callback->IsCancelled()) {
       // Another requestAnimationFrame callback already cancelled this one
       UseCounter::Count(context_,
@@ -89,18 +117,21 @@
     probe::AsyncTask async_task(context_, callback->async_task_context());
     probe::UserCallback probe(context_, "requestAnimationFrame", AtomicString(),
                               true);
-    if (callback->GetUseLegacyTimeBase())
+    if (callback->GetUseLegacyTimeBase()) {
       callback->Invoke(high_res_now_ms_legacy);
-    else
+    } else {
       callback->Invoke(high_res_now_ms);
+    }
   }
 
-  callbacks_to_invoke_.clear();
+  callbacks_to_invoke.clear();
 }
 
 void FrameRequestCallbackCollection::Trace(Visitor* visitor) const {
   visitor->Trace(frame_callbacks_);
   visitor->Trace(callbacks_to_invoke_);
+  visitor->Trace(internal_frame_callbacks_);
+  visitor->Trace(internal_callbacks_to_invoke_);
   visitor->Trace(context_);
 }
 
diff --git a/third_party/blink/renderer/core/dom/frame_request_callback_collection.h b/third_party/blink/renderer/core/dom/frame_request_callback_collection.h
index 2120faa7..2a42b02 100644
--- a/third_party/blink/renderer/core/dom/frame_request_callback_collection.h
+++ b/third_party/blink/renderer/core/dom/frame_request_callback_collection.h
@@ -66,6 +66,11 @@
   Member<V8FrameRequestCallback> callback_;
 };
 
+enum class FrameCallbackType {
+  kWebExposed,
+  kInternal,
+};
+
 class CORE_EXPORT FrameRequestCallbackCollection final : public NameClient {
   DISALLOW_NEW();
 
@@ -74,13 +79,14 @@
 
   using CallbackId = int;
 
-  CallbackId RegisterFrameCallback(FrameCallback*);
-  void CancelFrameCallback(CallbackId);
+  CallbackId RegisterFrameCallback(FrameCallback*, FrameCallbackType type);
+  void CancelFrameCallback(CallbackId, FrameCallbackType type);
   void ExecuteFrameCallbacks(double high_res_now_ms,
                              double high_res_now_ms_legacy);
 
-  bool HasFrameCallback() const { return frame_callbacks_.size(); }
-  bool IsEmpty() const { return !HasFrameCallback(); }
+  bool HasFrameCallback() const {
+    return frame_callbacks_.size() || internal_frame_callbacks_.size();
+  }
 
   void Trace(Visitor*) const;
   const char* GetHumanReadableName() const override {
@@ -90,12 +96,21 @@
  private:
   using CallbackList = HeapVector<Member<FrameCallback>>;
 
+  void ExecuteFrameCallbacksImpl(CallbackList& callbacks_to_invoke,
+                                 double high_res_now_ms,
+                                 double high_res_now_ms_legacy);
+
   CallbackList frame_callbacks_;
   // only non-empty while inside ExecuteCallbacks.
   CallbackList callbacks_to_invoke_;
 
   CallbackId next_callback_id_ = 0;
 
+  CallbackList internal_frame_callbacks_;
+  CallbackList internal_callbacks_to_invoke_;
+
+  CallbackId next_internal_callback_id_ = 0;
+
   Member<ExecutionContext> context_;
 };
 
diff --git a/third_party/blink/renderer/core/dom/scripted_animation_controller.cc b/third_party/blink/renderer/core/dom/scripted_animation_controller.cc
index 3a152ad..3eaee73 100644
--- a/third_party/blink/renderer/core/dom/scripted_animation_controller.cc
+++ b/third_party/blink/renderer/core/dom/scripted_animation_controller.cc
@@ -98,18 +98,20 @@
 }
 
 ScriptedAnimationController::CallbackId
-ScriptedAnimationController::RegisterFrameCallback(FrameCallback* callback) {
+ScriptedAnimationController::RegisterFrameCallback(FrameCallback* callback,
+                                                   FrameCallbackType type) {
   // If we no longer have a context, there is no need to register the callback.
   if (!GetExecutionContext()) {
     return 0;
   }
-  CallbackId id = callback_collection_.RegisterFrameCallback(callback);
+  CallbackId id = callback_collection_.RegisterFrameCallback(callback, type);
   ScheduleAnimationIfNeeded(cc::BeginMainFrameReason::kRAF);
   return id;
 }
 
-void ScriptedAnimationController::CancelFrameCallback(CallbackId id) {
-  callback_collection_.CancelFrameCallback(id);
+void ScriptedAnimationController::CancelFrameCallback(CallbackId id,
+                                                      FrameCallbackType type) {
+  callback_collection_.CancelFrameCallback(id, type);
 }
 
 bool ScriptedAnimationController::HasFrameCallback() const {
diff --git a/third_party/blink/renderer/core/dom/scripted_animation_controller.h b/third_party/blink/renderer/core/dom/scripted_animation_controller.h
index 4264fe3..ce1fdec 100644
--- a/third_party/blink/renderer/core/dom/scripted_animation_controller.h
+++ b/third_party/blink/renderer/core/dom/scripted_animation_controller.h
@@ -71,8 +71,8 @@
 
   // Animation frame callbacks are used for requestAnimationFrame().
   typedef int CallbackId;
-  CallbackId RegisterFrameCallback(FrameCallback*);
-  void CancelFrameCallback(CallbackId);
+  CallbackId RegisterFrameCallback(FrameCallback*, FrameCallbackType type);
+  void CancelFrameCallback(CallbackId, FrameCallbackType type);
   // Returns true if any callback is currently registered.
   bool HasFrameCallback() const;
 
diff --git a/third_party/blink/renderer/core/editing/markers/document_marker_controller.cc b/third_party/blink/renderer/core/editing/markers/document_marker_controller.cc
index 4d85b26..91c38c3 100644
--- a/third_party/blink/renderer/core/editing/markers/document_marker_controller.cc
+++ b/third_party/blink/renderer/core/editing/markers/document_marker_controller.cc
@@ -1464,7 +1464,7 @@
   glic_marker_animation_start_ = std::nullopt;
   glic_animation_state_ = GlicAnimationState::kRunning;
   auto* callback = MakeGarbageCollected<RequestAnimationFrameCallback>(this);
-  document_->RequestAnimationFrame(callback);
+  document_->RequestAnimationFrame(callback, FrameCallbackType::kInternal);
 }
 
 void DocumentMarkerController::ContinueGlicMarkerAnimation(
@@ -1495,7 +1495,7 @@
   }
 
   auto* callback = MakeGarbageCollected<RequestAnimationFrameCallback>(this);
-  document_->RequestAnimationFrame(callback);
+  document_->RequestAnimationFrame(callback, FrameCallbackType::kInternal);
 }
 
 bool DocumentMarkerController::UpdateGlicMarkerOpacity(
diff --git a/third_party/blink/renderer/core/frame/local_dom_window.cc b/third_party/blink/renderer/core/frame/local_dom_window.cc
index 0125fed7de..511fce6 100644
--- a/third_party/blink/renderer/core/frame/local_dom_window.cc
+++ b/third_party/blink/renderer/core/frame/local_dom_window.cc
@@ -197,7 +197,8 @@
 
   auto* frame_callback = MakeGarbageCollected<V8FrameCallback>(callback);
   frame_callback->SetUseLegacyTimeBase(legacy);
-  return document->RequestAnimationFrame(frame_callback);
+  return document->RequestAnimationFrame(frame_callback,
+                                         FrameCallbackType::kWebExposed);
 }
 
 }  // namespace
@@ -2217,7 +2218,7 @@
 }
 
 void LocalDOMWindow::cancelAnimationFrame(int id) {
-  document()->CancelAnimationFrame(id);
+  document()->CancelAnimationFrame(id, FrameCallbackType::kWebExposed);
 }
 
 bool LocalDOMWindow::originAgentCluster() const {
diff --git a/third_party/blink/renderer/core/html/html_marquee_element.cc b/third_party/blink/renderer/core/html/html_marquee_element.cc
index f0f96c36..3af928d 100644
--- a/third_party/blink/renderer/core/html/html_marquee_element.cc
+++ b/third_party/blink/renderer/core/html/html_marquee_element.cc
@@ -195,12 +195,14 @@
 
   RequestAnimationFrameCallback* callback =
       MakeGarbageCollected<RequestAnimationFrameCallback>(this);
-  continue_callback_request_id_ = GetDocument().RequestAnimationFrame(callback);
+  continue_callback_request_id_ = GetDocument().RequestAnimationFrame(
+      callback, FrameCallbackType::kInternal);
 }
 
 void HTMLMarqueeElement::stop() {
   if (continue_callback_request_id_) {
-    GetDocument().CancelAnimationFrame(continue_callback_request_id_);
+    GetDocument().CancelAnimationFrame(continue_callback_request_id_,
+                                       FrameCallbackType::kInternal);
     continue_callback_request_id_ = 0;
     return;
   }
diff --git a/third_party/blink/renderer/core/paint/timing/paint_timing.cc b/third_party/blink/renderer/core/paint/timing/paint_timing.cc
index 9612805a..5405f037 100644
--- a/third_party/blink/renderer/core/paint/timing/paint_timing.cc
+++ b/third_party/blink/renderer/core/paint/timing/paint_timing.cc
@@ -93,7 +93,7 @@
 
     if (auto* frame = paint_timing_->GetFrame()) {
       if (auto* document = frame->GetDocument()) {
-        document->RequestAnimationFrame(this);
+        document->RequestAnimationFrame(this, FrameCallbackType::kInternal);
       }
     }
   }
@@ -772,7 +772,8 @@
   // Cancel if there is already a registered callback.
   if (raf_after_bfcache_restore_measurement_callback_id_) {
     document->CancelAnimationFrame(
-        raf_after_bfcache_restore_measurement_callback_id_);
+        raf_after_bfcache_restore_measurement_callback_id_,
+        FrameCallbackType::kInternal);
     raf_after_bfcache_restore_measurement_callback_id_ = 0;
   }
 
@@ -780,7 +781,8 @@
       document->RequestAnimationFrame(
           MakeGarbageCollected<
               RecodingTimeAfterBackForwardCacheRestoreFrameCallback>(this,
-                                                                     index));
+                                                                     index),
+          FrameCallbackType::kInternal);
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/dom/scripted_animation_controller_test.cc b/third_party/blink/renderer/core/dom/scripted_animation_controller_test.cc
index c4001c15..b5473fe 100644
--- a/third_party/blink/renderer/core/dom/scripted_animation_controller_test.cc
+++ b/third_party/blink/renderer/core/dom/scripted_animation_controller_test.cc
@@ -184,7 +184,8 @@
   event->SetTarget(&GetDocument());
 
   Controller().RegisterFrameCallback(
-      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(1)));
+      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(1)),
+      FrameCallbackType::kWebExposed);
   Controller().EnqueueTask(observer.CreateTask(2));
   EXPECT_EQ(0u, observer.Order().size());
 
@@ -199,19 +200,22 @@
   TaskOrderObserver observer;
 
   Controller().RegisterFrameCallback(
-      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(1)));
+      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(1)),
+      FrameCallbackType::kWebExposed);
   EXPECT_TRUE(Controller().HasFrameCallback());
 
-  Controller().CancelFrameCallback(1);
+  Controller().CancelFrameCallback(1, FrameCallbackType::kWebExposed);
   EXPECT_FALSE(Controller().HasFrameCallback());
 
   Controller().RegisterFrameCallback(
-      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(1)));
+      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(1)),
+      FrameCallbackType::kWebExposed);
   Controller().RegisterFrameCallback(
-      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(2)));
+      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(2)),
+      FrameCallbackType::kWebExposed);
   EXPECT_TRUE(Controller().HasFrameCallback());
 
-  Controller().CancelFrameCallback(1);
+  Controller().CancelFrameCallback(1, FrameCallbackType::kWebExposed);
   EXPECT_TRUE(Controller().HasFrameCallback());
 
   // Servicing the scripted animations should call the remaining callback and
@@ -233,7 +237,8 @@
             *ran_callback = true;
           },
           WrapPersistent(&Controller()),
-          blink::subtle::UnretainedException(&ran_callback))));
+          blink::subtle::UnretainedException(&ran_callback))),
+      FrameCallbackType::kWebExposed);
 
   PageAnimator::ServiceScriptedAnimations(base::TimeTicks(),
                                           {{Controller(), false}});
@@ -242,4 +247,52 @@
   EXPECT_FALSE(Controller().GetExecutionContext()->IsInRequestAnimationFrame());
 }
 
+TEST_F(ScriptedAnimationControllerTest, TestInternalCallbackIsolation) {
+  TaskOrderObserver observer;
+
+  // Web-exposed callback registration gets ID 1 in web pool.
+  int web_id = Controller().RegisterFrameCallback(
+      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(1)),
+      FrameCallbackType::kWebExposed);
+  EXPECT_EQ(1, web_id);
+
+  // Internal callback registration gets independent ID 1 in internal pool.
+  int internal_id = Controller().RegisterFrameCallback(
+      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(2)),
+      FrameCallbackType::kInternal);
+  EXPECT_EQ(1, internal_id);
+
+  // Canceling internal callback with FrameCallbackType::kInternal should cancel
+  // internal without affecting web callback.
+  Controller().CancelFrameCallback(internal_id, FrameCallbackType::kInternal);
+
+  // Servicing scripted animations should run only the web callback.
+  PageAnimator::ServiceScriptedAnimations(base::TimeTicks(),
+                                          {{Controller(), false}});
+  EXPECT_EQ(1u, observer.Order().size());
+  EXPECT_EQ(1, observer.Order()[0]);
+}
+
+TEST_F(ScriptedAnimationControllerTest,
+       TestCancelWebCallbackDoesNotCancelInternalCallback) {
+  TaskOrderObserver observer;
+
+  int web_id = Controller().RegisterFrameCallback(
+      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(1)),
+      FrameCallbackType::kWebExposed);
+  Controller().RegisterFrameCallback(
+      MakeGarbageCollected<RunTaskCallback>(observer.CreateTask(2)),
+      FrameCallbackType::kInternal);
+
+  // CancelFrameCallback with kWebExposed cancels web callback without
+  // affecting internal callback.
+  Controller().CancelFrameCallback(web_id, FrameCallbackType::kWebExposed);
+
+  // Servicing scripted animations should run only the internal callback.
+  PageAnimator::ServiceScriptedAnimations(base::TimeTicks(),
+                                          {{Controller(), false}});
+  EXPECT_EQ(1u, observer.Order().size());
+  EXPECT_EQ(2, observer.Order()[0]);
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/frame/document_loading_rendering_test.cc b/third_party/blink/renderer/core/frame/document_loading_rendering_test.cc
index 786c949..223d8dc4 100644
--- a/third_party/blink/renderer/core/frame/document_loading_rendering_test.cc
+++ b/third_party/blink/renderer/core/frame/document_loading_rendering_test.cc
@@ -343,7 +343,8 @@
 
   // Frame while the child frame still has pending sheets.
   auto* frame1_callback = MakeGarbageCollected<CheckRafCallback>();
-  child_frame->contentDocument()->RequestAnimationFrame(frame1_callback);
+  child_frame->contentDocument()->RequestAnimationFrame(
+      frame1_callback, FrameCallbackType::kWebExposed);
   auto frame1 = Compositor().BeginFrame();
   EXPECT_FALSE(frame1_callback->WasCalled());
   EXPECT_TRUE(frame1.Contains(SimCanvas::kRect, "red"));
@@ -356,7 +357,8 @@
 
   // Frame with all lifecycle updates enabled.
   auto* frame2_callback = MakeGarbageCollected<CheckRafCallback>();
-  child_frame->contentDocument()->RequestAnimationFrame(frame2_callback);
+  child_frame->contentDocument()->RequestAnimationFrame(
+      frame2_callback, FrameCallbackType::kWebExposed);
   auto frame2 = Compositor().BeginFrame();
   EXPECT_TRUE(frame1_callback->WasCalled());
   EXPECT_TRUE(frame2_callback->WasCalled());
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.