Chrome · Scheduling › Scheduler
CVE-2026-113184
Memory Corruption in Scheduling (Scheduler)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/scheduler/scripted_idle_task_controller.cc |
modified | |
CORE_EXPORTthird_party/blink/renderer/core/scheduler/scripted_idle_task_controller.h |
modified | |
V8IdleTaskthird_party/blink/renderer/core/scheduler/window_idle_tasks.cc |
modified | |
WindowIdleTasksManagerthird_party/blink/renderer/core/scheduler/window_idle_tasks_manager.cc |
modified |
Files Changed
third_party/blink/renderer/core/scheduler/scripted_idle_task_controller.hthird_party/blink/renderer/core/scheduler/window_idle_tasks_manager_test.ccthird_party/blink/renderer/core/scheduler/window_idle_tasks_manager.ccthird_party/blink/renderer/core/scheduler/build.gnithird_party/blink/renderer/core/scheduler/window_idle_tasks.ccthird_party/blink/renderer/core/scheduler/scripted_idle_task_controller.ccthird_party/blink/renderer/core/scheduler/scripted_idle_task_controller_test.ccthird_party/blink/renderer/core/scheduler/window_idle_tasks.idlthird_party/blink/renderer/core/loader/preload_helper.ccthird_party/blink/renderer/core/scheduler/window_idle_tasks.hthird_party/blink/renderer/core/scheduler/window_idle_tasks_manager.hthird_party/blink/renderer/core/editing/spellcheck/idle_spell_check_controller.cc
Patch
From c618062d4798e8f4d2982a1cc2e71d45e211b4e9 Mon Sep 17 00:00:00 2001
From: Scott Haseley <shaseley@chromium.org>
Date: Thu, 17 Sep 2026 05:49:14 -0700
Subject: [PATCH] [scheduler] Use a separate id space for web-exposed idle tasks
ScriptedIdleTaskController is used for both web-exposed idle tasks
(requestIdleCallback) and as subset of internal idle tasks, e.g. spell
check. But since they share the same ID space and tasks are cancellable
by ID, web exposed tasks can cancel internal idle tasks.
This CL fixes this by using a separate web-exposed ID space and mapping
it to the global callback ID space.
- WindowIdleTasksManager is introduced to manage the ID mapping
- The logic from WindowIdleTasks is moved to WindowIdleTasksManager
- WindowIdleTasksManager now owns ScriptedTaskIdleController, which
is mostly otherwise unchanged.
Also:
- Change requestIdleCallback and cancelIdleCallback to use unsigned
long, matching the spec.
Note: This is a bit different than how a similar issue was fixed for
requestAnimationFrame (changed to maintain separate lists of callbacks),
because the underlying callback list maintenance and scheduling is more
complicated.
Fixed: 540068924
Change-Id: Ida13c8d0fd22a77c8a2b6b295aeeff8557d795d8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8402543
Reviewed-by: Michal Mocny <mmocny@chromium.org>
Reviewed-by: Nate Chapin <japhet@chromium.org>
Commit-Queue: Scott Haseley <shaseley@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699719}
---
diff --git a/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_controller.cc b/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_controller.cc
index 4d7405b..d167d3f 100644
--- a/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_controller.cc
+++ b/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_controller.cc
@@ -6,7 +6,6 @@
#include <array>
-#include "base/check_deref.h"
#include "base/debug/crash_logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
@@ -101,7 +100,7 @@
void IdleSpellCheckController::DisposeIdleCallback() {
if (idle_callback_handle_ != kInvalidHandle && GetExecutionContext()) {
- ScriptedIdleTaskController::From(*GetExecutionContext())
+ ScriptedIdleTaskController::From(GetWindow())
.CancelCallback(idle_callback_handle_);
}
idle_callback_handle_ = kInvalidHandle;
@@ -232,7 +231,7 @@
IdleRequestOptions* options = IdleRequestOptions::Create();
options->setTimeout(kHotModeRequestTimeoutMS);
idle_callback_handle_ =
- ScriptedIdleTaskController::From(CHECK_DEREF(GetExecutionContext()))
+ ScriptedIdleTaskController::From(GetWindow())
.RegisterCallback(MakeGarbageCollected<IdleCallback>(this), options);
state_ = State::kHotModeRequested;
}
@@ -264,7 +263,7 @@
}
idle_callback_handle_ =
- ScriptedIdleTaskController::From(CHECK_DEREF(GetExecutionContext()))
+ ScriptedIdleTaskController::From(GetWindow())
.RegisterCallback(MakeGarbageCollected<IdleCallback>(this),
IdleRequestOptions::Create());
state_ = State::kColdModeRequested;
@@ -395,7 +394,7 @@
case State::kHotModeRequested:
case State::kColdModeRequested:
if (GetExecutionContext()) {
- ScriptedIdleTaskController::From(*GetExecutionContext())
+ ScriptedIdleTaskController::From(GetWindow())
.CancelCallback(idle_callback_handle_);
}
Invoke(deadline);
diff --git a/third_party/blink/renderer/core/loader/preload_helper.cc b/third_party/blink/renderer/core/loader/preload_helper.cc
index 8118a43..3164708 100644
--- a/third_party/blink/renderer/core/loader/preload_helper.cc
+++ b/third_party/blink/renderer/core/loader/preload_helper.cc
@@ -6,6 +6,7 @@
#include <utility>
+#include "base/check_deref.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
@@ -1080,7 +1081,7 @@
document.GetExecutionContext()->GetSecurityOrigin(), cross_origin);
}
IdleRequestOptions* idle_options = IdleRequestOptions::Create();
- ScriptedIdleTaskController::From(*document.GetExecutionContext())
+ ScriptedIdleTaskController::From(CHECK_DEREF(document.domWindow()))
.RegisterCallback(MakeGarbageCollected<LoadDictionaryWhenIdleTask>(
std::move(link_fetch_params), document.Fetcher(),
pending_preload),
diff --git a/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller.cc b/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller.cc
index d6ad54b..1fbde660 100644
--- a/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller.cc
+++ b/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller.cc
@@ -16,6 +16,7 @@
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
+#include "third_party/blink/renderer/core/scheduler/window_idle_tasks_manager.h"
#include "third_party/blink/renderer/platform/heap/persistent.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
@@ -88,25 +89,15 @@
CHECK(!delayed_task_handle_.IsValid());
}
-const char ScriptedIdleTaskController::kSupplementName[] =
- "ScriptedIdleTaskController";
-
// static
ScriptedIdleTaskController& ScriptedIdleTaskController::From(
- ExecutionContext& context) {
- ScriptedIdleTaskController* controller =
- Supplement<ExecutionContext>::From<ScriptedIdleTaskController>(&context);
- if (!controller) {
- controller = MakeGarbageCollected<ScriptedIdleTaskController>(&context);
- Supplement<ExecutionContext>::ProvideTo(context, controller);
- }
- return *controller;
+ LocalDOMWindow& window) {
+ return WindowIdleTasksManager::From(window).GetScriptedIdleTaskController();
}
ScriptedIdleTaskController::ScriptedIdleTaskController(
ExecutionContext* context)
: ExecutionContextLifecycleStateObserver(context),
- Supplement<ExecutionContext>(*context),
scheduler_(ThreadScheduler::Current()) {
UpdateStateIfNeeded();
}
@@ -118,7 +109,6 @@
void ScriptedIdleTaskController::Trace(Visitor* visitor) const {
visitor->Trace(idle_tasks_);
ExecutionContextLifecycleStateObserver::Trace(visitor);
- Supplement<ExecutionContext>::Trace(visitor);
}
int ScriptedIdleTaskController::NextCallbackId() {
@@ -227,9 +217,9 @@
DEVTOOLS_TIMELINE_TRACE_EVENT_INSTANT(
"CancelIdleCallback", inspector_idle_callback_cancel_event::Data,
GetExecutionContext(), id);
- if (!IsValidCallbackId(id)) {
- return;
- }
+ // The web-exposed layer (WindowIdleTasksManager) should never send invalid
+ // IDs, nor should other internal clients.
+ CHECK(IsValidCallbackId(id));
RemoveIdleTask(id);
diff --git a/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller.h b/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller.h
index 07a8b6ee..9b19662 100644
--- a/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller.h
+++ b/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller.h
@@ -18,7 +18,6 @@
#include "third_party/blink/renderer/platform/heap/collection_support/heap_hash_map.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/heap/prefinalizer.h"
-#include "third_party/blink/renderer/platform/supplementable.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
namespace blink {
@@ -62,7 +61,6 @@
class CORE_EXPORT ScriptedIdleTaskController
: public GarbageCollected<ScriptedIdleTaskController>,
public ExecutionContextLifecycleStateObserver,
- public Supplement<ExecutionContext>,
public NameClient {
USING_PRE_FINALIZER(ScriptedIdleTaskController, Dispose);
@@ -87,9 +85,7 @@
RefCountedCounter counter_;
};
- static const char kSupplementName[];
-
- static ScriptedIdleTaskController& From(ExecutionContext& context);
+ static ScriptedIdleTaskController& From(LocalDOMWindow&);
explicit ScriptedIdleTaskController(ExecutionContext*);
~ScriptedIdleTaskController() override;
diff --git a/third_party/blink/renderer/core/scheduler/window_idle_tasks.cc b/third_party/blink/renderer/core/scheduler/window_idle_tasks.cc
index 62afa05e..a37c33cd 100644
--- a/third_party/blink/renderer/core/scheduler/window_idle_tasks.cc
+++ b/third_party/blink/renderer/core/scheduler/window_idle_tasks.cc
@@ -4,87 +4,22 @@
#include "third_party/blink/renderer/core/scheduler/window_idle_tasks.h"
-#include <optional>
-
-#include "third_party/blink/public/common/scheduler/task_attribution_id.h"
-#include "third_party/blink/renderer/bindings/core/v8/v8_idle_request_callback.h"
-#include "third_party/blink/renderer/bindings/core/v8/v8_idle_request_options.h"
-#include "third_party/blink/renderer/core/execution_context/execution_context.h"
-#include "third_party/blink/renderer/core/frame/local_dom_window.h"
-#include "third_party/blink/renderer/core/scheduler/dom_scheduler.h"
-#include "third_party/blink/renderer/core/scheduler/dom_task_signal.h"
-#include "third_party/blink/renderer/core/scheduler/scheduler_task_context.h"
-#include "third_party/blink/renderer/core/scheduler/scripted_idle_task_controller.h"
-#include "third_party/blink/renderer/core/scheduler/task_attribution_util.h"
-#include "third_party/blink/renderer/core/scheduler/web_scheduling_task_state.h"
-#include "third_party/blink/renderer/platform/bindings/script_state.h"
-#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
-#include "third_party/blink/renderer/platform/scheduler/public/task_attribution_info.h"
-#include "third_party/blink/renderer/platform/scheduler/public/task_attribution_tracker.h"
-#include "third_party/blink/renderer/platform/scheduler/public/web_scheduling_priority.h"
+#include "base/types/pass_key.h"
+#include "third_party/blink/renderer/core/scheduler/window_idle_tasks_manager.h"
namespace blink {
-namespace {
-
-// `V8IdleTask` is the adapter class for the conversion from
-// `V8IdleRequestCallback` to `IdleTask`.
-class V8IdleTask : public IdleTask {
- public:
- static V8IdleTask* Create(V8IdleRequestCallback* callback,
- ExecutionContext* scheduling_context) {
- return MakeGarbageCollected<V8IdleTask>(callback, scheduling_context);
- }
-
- V8IdleTask(V8IdleRequestCallback* callback,
- ExecutionContext* scheduling_context)
- : callback_(callback) {
- ScriptState* script_state = callback_->CallbackRelevantScriptState();
- auto* signal =
- DOMScheduler::scheduler(*scheduling_context)
- ->GetFixedPriorityTaskSignal(
- script_state, WebSchedulingPriority::kBackgroundPriority);
- web_scheduling_task_state_ = MakeGarbageCollected<WebSchedulingTaskState>(
- CaptureCurrentTaskState(scheduling_context),
- MakeGarbageCollected<SchedulerTaskContext>(
- scheduling_context, /*abort_source=*/nullptr, signal));
- }
-
- ~V8IdleTask() override = default;
-
- void invoke(IdleDeadline* deadline) override {
- std::optional<scheduler::TaskAttributionTracker::TaskScope>
- task_attribution_scope;
- if (auto* tracker =
- scheduler::TaskAttributionTracker::From(callback_->GetIsolate())) {
- task_attribution_scope = tracker->SetCurrentTaskState(
- web_scheduling_task_state_, TaskScopeType::kRequestIdleCallback);
- }
- callback_->InvokeAndReportException(nullptr, deadline);
- }
-
- void Trace(Visitor* visitor) const override {
- visitor->Trace(callback_);
- visitor->Trace(web_scheduling_task_state_);
- IdleTask::Trace(visitor);
- }
-
- private:
- Member<V8IdleRequestCallback> callback_;
- Member<WebSchedulingTaskState> web_scheduling_task_state_;
-};
-
-} // namespace
-
-int WindowIdleTasks::requestIdleCallback(LocalDOMWindow& window,
- V8IdleRequestCallback* callback,
- const IdleRequestOptions* options) {
- return ScriptedIdleTaskController::From(window).RegisterCallback(
- V8IdleTask::Create(callback, &window), options);
+uint32_t WindowIdleTasks::requestIdleCallback(
+ LocalDOMWindow& window,
+ V8IdleRequestCallback* callback,
+ const IdleRequestOptions* options) {
+ return WindowIdleTasksManager::From(window).RequestIdleCallback(
+ callback, options, base::PassKey<WindowIdleTasks>{});
}
-void WindowIdleTasks::cancelIdleCallback(LocalDOMWindow& window, int id) {
- ScriptedIdleTaskController::From(window).CancelCallback(id);
+void WindowIdleTasks::cancelIdleCallback(LocalDOMWindow& window, uint32_t id) {
+ WindowIdleTasksManager::From(window).CancelIdleCallback(
+ id, base::PassKey<WindowIdleTasks>{});
}
} // namespace blink
diff --git a/third_party/blink/renderer/core/scheduler/window_idle_tasks.h b/third_party/blink/renderer/core/scheduler/window_idle_tasks.h
index 81babc35..45f41a6 100644
--- a/third_party/blink/renderer/core/scheduler/window_idle_tasks.h
+++ b/third_party/blink/renderer/core/scheduler/window_idle_tasks.h
@@ -5,6 +5,8 @@
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_SCHEDULER_WINDOW_IDLE_TASKS_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_SCHEDULER_WINDOW_IDLE_TASKS_H_
+#include <cstdint>
+
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
@@ -17,10 +19,10 @@
STATIC_ONLY(WindowIdleTasks);
public:
- static int requestIdleCallback(LocalDOMWindow&,
- V8IdleRequestCallback*,
- const IdleRequestOptions*);
- static void cancelIdleCallback(LocalDOMWindow&, int id);
+ static uint32_t requestIdleCallback(LocalDOMWindow&,
+ V8IdleRequestCallback*,
+ const IdleRequestOptions*);
+ static void cancelIdleCallback(LocalDOMWindow&, uint32_t id);
};
} // namespace blink
diff --git a/third_party/blink/renderer/core/scheduler/window_idle_tasks.idl b/third_party/blink/renderer/core/scheduler/window_idle_tasks.idl
index cc3c089..5b023bd 100644
--- a/third_party/blink/renderer/core/scheduler/window_idle_tasks.idl
+++ b/third_party/blink/renderer/core/scheduler/window_idle_tasks.idl
@@ -7,6 +7,6 @@
[
ImplementedAs=WindowIdleTasks
] partial interface Window {
- [Measure] long requestIdleCallback(IdleRequestCallback callback, optional IdleRequestOptions options = {});
- void cancelIdleCallback(long handle);
+ [Measure] unsigned long requestIdleCallback(IdleRequestCallback callback, optional IdleRequestOptions options = {});
+ void cancelIdleCallback(unsigned long handle);
};
diff --git a/third_party/blink/renderer/core/scheduler/window_idle_tasks_manager.cc b/third_party/blink/renderer/core/scheduler/window_idle_tasks_manager.cc
new file mode 100644
index 0000000..fb53ab9
--- /dev/null
+++ b/third_party/blink/renderer/core/scheduler/window_idle_tasks_manager.cc
@@ -0,0 +1,174 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/scheduler/window_idle_tasks_manager.h"
+
+#include <optional>
+
+#include "base/check_op.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_idle_request_callback.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_idle_request_options.h"
+#include "third_party/blink/renderer/core/execution_context/execution_context.h"
+#include "third_party/blink/renderer/core/frame/local_dom_window.h"
+#include "third_party/blink/renderer/core/scheduler/dom_scheduler.h"
+#include "third_party/blink/renderer/core/scheduler/dom_task_signal.h"
+#include "third_party/blink/renderer/core/scheduler/scheduler_task_context.h"
+#include "third_party/blink/renderer/core/scheduler/task_attribution_util.h"
+#include "third_party/blink/renderer/core/scheduler/web_scheduling_task_state.h"
+#include "third_party/blink/renderer/platform/bindings/script_state.h"
+#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+#include "third_party/blink/renderer/platform/scheduler/public/task_attribution_info.h"
+#include "third_party/blink/renderer/platform/scheduler/public/task_attribution_tracker.h"
+#include "third_party/blink/renderer/platform/scheduler/public/web_scheduling_priority.h"
+
+namespace blink {
+
+namespace {
+
+// The valid id range is 1 to int max. int max is used instead of uint32_t max
+// to keep the value in the 31-bit SMI range, and to be consistent with other
+// IDs, e.g. setTimeout().
+constexpr uint32_t kMaxCallbackId = std::numeric_limits<int>::max();
+
+} // namespace
+
+// `V8IdleTask` is the adapter class for the conversion from
+// `V8IdleRequestCallback` to `IdleTask`.
+class WindowIdleTasksManager::V8IdleTask : public IdleTask {
+ public:
+ V8IdleTask(uint32_t id,
+ WindowIdleTasksManager* manager,
+ V8IdleRequestCallback* callback,
+ ExecutionContext* scheduling_context)
+ : id_(id), window_idle_tasks_manager_(manager), callback_(callback) {
+ ScriptState* script_state = callback_->CallbackRelevantScriptState();
+ auto* signal =
+ DOMScheduler::scheduler(*scheduling_context)
+ ->GetFixedPriorityTaskSignal(
+ script_state, WebSchedulingPriority::kBackgroundPriority);
+ web_scheduling_task_state_ = MakeGarbageCollected<WebSchedulingTaskState>(
+ CaptureCurrentTaskState(scheduling_context),
+ MakeGarbageCollected<SchedulerTaskContext>(
+ scheduling_context, /*abort_source=*/nullptr, signal));
+ }
+
... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller_test.cc b/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller_test.cc
index 4ba1cf43..a0cc91d 100644
--- a/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller_test.cc
+++ b/third_party/blink/renderer/core/scheduler/scripted_idle_task_controller_test.cc
@@ -267,9 +267,12 @@
execution_context_.emplace(
std::make_unique<IdleTaskControllerFrameScheduler>(
&scheduler_.value()));
+ controller_ = MakeGarbageCollected<ScriptedIdleTaskController>(
+ &execution_context_->GetExecutionContext());
}
void DeleteScheduler() {
+ controller_.Clear();
execution_context_.reset();
scheduler_overrider_.reset();
scheduler_.reset();
@@ -277,16 +280,14 @@
void DeleteExecutionContext() { execution_context_.reset(); }
- ScriptedIdleTaskController* GetController() {
- return &ScriptedIdleTaskController::From(
- execution_context_->GetExecutionContext());
- }
+ ScriptedIdleTaskController* GetController() { return controller_.Get(); }
protected:
test::TaskEnvironment task_environment_;
std::optional<MockScriptedIdleTaskControllerScheduler> scheduler_;
private:
+ Persistent<ScriptedIdleTaskController> controller_;
std::optional<ScopedSchedulerOverrider> scheduler_overrider_;
std::optional<ScopedNullExecutionContext> execution_context_;
};
diff --git a/third_party/blink/renderer/core/scheduler/window_idle_tasks_manager_test.cc b/third_party/blink/renderer/core/scheduler/window_idle_tasks_manager_test.cc
new file mode 100644
index 0000000..11a81d1d
--- /dev/null
+++ b/third_party/blink/renderer/core/scheduler/window_idle_tasks_manager_test.cc
@@ -0,0 +1,382 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/scheduler/window_idle_tasks_manager.h"
+
+#include <limits>
+#include <memory>
+#include <optional>
+
+#include "base/time/time.h"
+#include "base/types/pass_key.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_idle_request_callback.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_idle_request_options.h"
+#include "third_party/blink/renderer/core/frame/local_dom_window.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/scheduler/scripted_idle_task_controller.h"
+#include "third_party/blink/renderer/core/scheduler/window_idle_tasks.h"
+#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
+#include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+#include "v8/include/v8-external.h"
+#include "v8/include/v8.h"
+
+namespace blink {
+
+namespace {
+
+class MockIdleTask : public IdleTask {
+ public:
+ MOCK_METHOD1(invoke, void(IdleDeadline*));
+};
+
+void IncrementCallCount(const v8::FunctionCallbackInfo<v8::Value>& info) {
+ auto* call_count = static_cast<int*>(info.Data().As<v8::External>()->Value(
+ v8::kExternalPointerTypeTagDefault));
+ ++(*call_count);
+}
+
+} // namespace
+
+class WindowIdleTasksManagerTest : public PageTestBase {
+ public:
+ WindowIdleTasksManagerTest()
+ : PageTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
+
+ void SetUp() override {
+ EnablePlatform();
+ PageTestBase::SetUp();
+ GetFrame().GetSettings()->SetScriptEnabled(true);
+ }
+
+ WindowIdleTasksManager& Manager() {
+ return WindowIdleTasksManager::From(*GetFrame().DomWindow());
+ }
+
+ base::PassKey<WindowIdleTasksManagerTest> PassKey() {
+ return base::PassKey<WindowIdleTasksManagerTest>();
+ }
+
+ V8IdleRequestCallback* CreateIdleCallback() {
+ ScriptState* script_state = ToScriptStateForMainWorld(&GetFrame());
+ ScriptState::Scope scope(script_state);
+ v8::Local<v8::Function> fn =
+ v8::Function::New(script_state->GetContext(), nullptr).ToLocalChecked();
+ return V8IdleRequestCallback::Create(fn);
+ }
+
+ V8IdleRequestCallback* CreateCountingIdleCallback(int* call_count) {
+ ScriptState* script_state = ToScriptStateForMainWorld(&GetFrame());
+ ScriptState::Scope scope(script_state);
+ v8::Local<v8::External> data =
+ v8::External::New(script_state->GetIsolate(), call_count,
+ v8::kExternalPointerTypeTagDefault);
+ v8::Local<v8::Function> fn =
+ v8::Function::New(script_state->GetContext(), IncrementCallCount, data)
+ .ToLocalChecked();
+ return V8IdleRequestCallback::Create(fn);
+ }
+
+ void SetIdleCallbackIdentifier(uint32_t id) {
+ Manager().idle_callback_identifier_ = id;
+ }
+
+ bool HasWebExposedCallback(uint32_t id) {
+ return Manager().web_exposed_ids_to_callback_ids_.Contains(id);
+ }
+
+ size_t WebExposedCallbackCount() {
+ return WebExposedCallbackCount(Manager());
+ }
+
+ size_t WebExposedCallbackCount(WindowIdleTasksManager& manager) {
+ return manager.web_exposed_ids_to_callback_ids_.size();
+ }
+
+ std::optional<ScriptedIdleTaskController::CallbackId> GetInternalCallbackId(
+ uint32_t id) {
+ auto it = Manager().web_exposed_ids_to_callback_ids_.find(id);
+ if (it == Manager().web_exposed_ids_to_callback_ids_.end()) {
+ return std::nullopt;
+ }
+ return it->value;
+ }
+};
+
+TEST_F(WindowIdleTasksManagerTest, FromReturnsSameInstanceAndController) {
+ LocalDOMWindow* window = GetFrame().DomWindow();
+ WindowIdleTasksManager& manager1 = WindowIdleTasksManager::From(*window);
+ WindowIdleTasksManager& manager2 = WindowIdleTasksManager::From(*window);
+ EXPECT_EQ(&manager1, &manager2);
+
+ ScriptedIdleTaskController& controller =
+ ScriptedIdleTaskController::From(*window);
+ EXPECT_EQ(&manager1.GetScriptedIdleTaskController(), &controller);
+}
+
+TEST_F(WindowIdleTasksManagerTest, RequestIdleCallbackAllocatesSequentialIds) {
+ uint32_t id1 = Manager().RequestIdleCallback(
+ CreateIdleCallback(), IdleRequestOptions::Create(), PassKey());
+ uint32_t id2 = Manager().RequestIdleCallback(
+ CreateIdleCallback(), IdleRequestOptions::Create(), PassKey());
+ uint32_t id3 = Manager().RequestIdleCallback(
+ CreateIdleCallback(), IdleRequestOptions::Create(), PassKey());
+
+ EXPECT_EQ(id1, 1u);
+ EXPECT_EQ(id2, 2u);
+ EXPECT_EQ(id3, 3u);
+
+ EXPECT_EQ(WebExposedCallbackCount(), 3u);
+ EXPECT_TRUE(HasWebExposedCallback(id1));
+ EXPECT_TRUE(HasWebExposedCallback(id2));
+ EXPECT_TRUE(HasWebExposedCallback(id3));
+
+ auto internal_id1 = GetInternalCallbackId(id1);
+ auto internal_id2 = GetInternalCallbackId(id2);
+ auto internal_id3 = GetInternalCallbackId(id3);
+ ASSERT_TRUE(internal_id1.has_value());
+ ASSERT_TRUE(internal_id2.has_value());
+ ASSERT_TRUE(internal_id3.has_value());
+
+ EXPECT_TRUE(
+ Manager().GetScriptedIdleTaskController().HasCallback(*internal_id1));
+ EXPECT_TRUE(
+ Manager().GetScriptedIdleTaskController().HasCallback(*internal_id2));
+ EXPECT_TRUE(
+ Manager().GetScriptedIdleTaskController().HasCallback(*internal_id3));
+}
+
+TEST_F(WindowIdleTasksManagerTest, CancelIdleCallback) {
+ uint32_t id1 = Manager().RequestIdleCallback(
+ CreateIdleCallback(), IdleRequestOptions::Create(), PassKey());
+ uint32_t id2 = Manager().RequestIdleCallback(
+ CreateIdleCallback(), IdleRequestOptions::Create(), PassKey());
+ uint32_t id3 = Manager().RequestIdleCallback(
+ CreateIdleCallback(), IdleRequestOptions::Create(), PassKey());
+
+ auto internal_id2 = GetInternalCallbackId(id2);
+ ASSERT_TRUE(internal_id2.has_value());
+
+ Manager().CancelIdleCallback(id2, PassKey());
+
+ EXPECT_EQ(WebExposedCallbackCount(), 2u);
+ EXPECT_TRUE(HasWebExposedCallback(id1));
+ EXPECT_FALSE(HasWebExposedCallback(id2));
+ EXPECT_TRUE(HasWebExposedCallback(id3));
+
+ // The underlying task should also be cancelled in ScriptedIdleTaskController.
+ EXPECT_FALSE(
+ Manager().GetScriptedIdleTaskController().HasCallback(*internal_id2));
+
+ // Cancelling a nonexistent ID is a safe no-op.
+ Manager().CancelIdleCallback(0, PassKey());
+ Manager().CancelIdleCallback(9999, PassKey());
+ EXPECT_EQ(WebExposedCallbackCount(), 2u);
+}
+
+TEST_F(WindowIdleTasksManagerTest, CancelViaWindowIdleTasks) {
+ LocalDOMWindow* window = GetFrame().DomWindow();
+ uint32_t id = WindowIdleTasks::requestIdleCallback(
+ *window, CreateIdleCallback(), IdleRequestOptions::Create());
+ EXPECT_EQ(id, 1u);
+ EXPECT_TRUE(HasWebExposedCallback(id));
+
+ WindowIdleTasks::cancelIdleCallback(*window, id);
+ EXPECT_FALSE(HasWebExposedCallback(id));
+ EXPECT_EQ(WebExposedCallbackCount(), 0u);
+}
+
+// Regression test for crbug.com/540068924: Web-exposed idle callbacks must have
+// a separate ID namespace from internal Blink idle tasks so that web pages
+// cannot detect or cancel internal tasks (e.g. spell check).
+TEST_F(WindowIdleTasksManagerTest, InternalCallbackIsolation) {
+ LocalDOMWindow* window = GetFrame().DomWindow();
+
+ // 1. Web-exposed callback gets ID 1.
+ uint32_t web_id1 = WindowIdleTasks::requestIdleCallback(
+ *window, CreateIdleCallback(), IdleRequestOptions::Create());
+ EXPECT_EQ(web_id1, 1u);
+
+ // 2. An internal idle task is registered directly on
+ // ScriptedIdleTaskController.
+ auto* internal_task1 = MakeGarbageCollected<MockIdleTask>();
+ ScriptedIdleTaskController::CallbackId internal_id1 =
+ ScriptedIdleTaskController::From(*window).RegisterCallback(
+ internal_task1, IdleRequestOptions::Create());
+ EXPECT_TRUE(
+ ScriptedIdleTaskController::From(*window).HasCallback(internal_id1));
+
+ // 3. Web-exposed callback gets ID 2 (no gap caused by internal callback).
+ uint32_t web_id2 = WindowIdleTasks::requestIdleCallback(
+ *window, CreateIdleCallback(), IdleRequestOptions::Create());
+ EXPECT_EQ(web_id2, 2u);
+
+ // 4. Register a second internal idle task. Its internal CallbackId is 4
+ // (internal IDs are 1, 2, 3, 4), which does not match any web-exposed ID.
+ auto* internal_task2 = MakeGarbageCollected<MockIdleTask>();
+ ScriptedIdleTaskController::CallbackId internal_id2 =
+ ScriptedIdleTaskController::From(*window).RegisterCallback(
+ internal_task2, IdleRequestOptions::Create());
+ EXPECT_TRUE(
+ ScriptedIdleTaskController::From(*window).HasCallback(internal_id2));
+ EXPECT_NE(static_cast<uint32_t>(internal_id2), web_id1);
+ EXPECT_NE(static_cast<uint32_t>(internal_id2), web_id2);
+
+ // Calling WindowIdleTasks::cancelIdleCallback with internal_id2 must NOT
+ // cancel the internal task.
+ WindowIdleTasks::cancelIdleCallback(*window,
+ static_cast<uint32_t>(internal_id2));
+ EXPECT_TRUE(
+ ScriptedIdleTaskController::From(*window).HasCallback(internal_id2));
+
+ // Web callbacks must remain intact.
+ EXPECT_TRUE(HasWebExposedCallback(web_id1));
+ EXPECT_TRUE(HasWebExposedCallback(web_id2));
+
+ // 5. Internal tasks can still be cancelled directly via
+ // ScriptedIdleTaskController.
+ ScriptedIdleTaskController::From(*window).CancelCallback(internal_id1);
+ ScriptedIdleTaskController::From(*window).CancelCallback(internal_id2);
+ EXPECT_FALSE(
+ ScriptedIdleTaskController::From(*window).HasCallback(internal_id1));
+ EXPECT_FALSE(
+ ScriptedIdleTaskController::From(*window).HasCallback(internal_id2));
+
+ // Web callbacks are still unaffected.
+ EXPECT_TRUE(HasWebExposedCallback(web_id1));
+ EXPECT_TRUE(HasWebExposedCallback(web_id2));
+}
+
+TEST_F(WindowIdleTasksManagerTest,
+ CancelWebCallbackDoesNotCancelInternalCallbackWithSameId) {
+ LocalDOMWindow* window = GetFrame().DomWindow();
+
... (truncated)
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.
References
On This Page