CVE-2026-11673
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/services/auction_worklet/bidder_worklet_unittest.cc |
modified | |
whilecontent/services/auction_worklet/bidder_worklet_unittest.cc |
modified | |
BidderWorkletRealTimeReportingEnabledTestcontent/services/auction_worklet/bidder_worklet_unittest.cc |
modified | |
BidderWorkletRealTimeReportingEnabledTestcontent/services/auction_worklet/bidder_worklet_unittest.cc |
modified | |
forcontent/services/auction_worklet/context_recycler.cc |
modified |
Files Changed
content/services/auction_worklet/bidder_worklet_unittest.cccontent/services/auction_worklet/context_recycler.cc
Patch
From 5cfe401fdda609ebf3333c2733b78e3e585207fe Mon Sep 17 00:00:00 2001 From: Maks Orlovich <[email protected]> Date: Wed, 27 May 2026 11:37:37 -0700 Subject: [PATCH] FLEDGE: Fix wrong timing of microtask checkpoint execution Bug: 516902973 Change-Id: Ibac10bc5f07b40ca9cd984fe30e5950f7c12a7ff Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7877578 Commit-Queue: Maks Orlovich <[email protected]> Reviewed-by: Russ Hamilton <[email protected]> Cr-Commit-Position: refs/heads/main@{#1637121} --- diff --git a/content/services/auction_worklet/bidder_worklet_unittest.cc b/content/services/auction_worklet/bidder_worklet_unittest.cc index 743b2bde..5805ba89 100644 --- a/content/services/auction_worklet/bidder_worklet_unittest.cc +++ b/content/services/auction_worklet/bidder_worklet_unittest.cc @@ -15091,6 +15091,41 @@ BidderWorklet::SignalsOriginRelation::kCrossOriginSignals, 5); } +TEST_F(BidderWorkletTest, MicroTaskTiming) { + const char kScript[] = R"( + function generateBid() { + Promise.resolve().then(() => { + /* If this runs at wrong time, the error will be top-level timeout, + * not generateBid one */ + while(true) {} + }); + + while(true) {}; + } + )"; + + mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet(); + AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_, + kScript); + GenerateBid(bidder_worklet.get()); + generate_bid_run_loop_ = std::make_unique<base::RunLoop>(); + generate_bid_run_loop_->Run(); + EXPECT_EQ(0u, bids_.size()); + EXPECT_THAT(bid_errors_, + testing::ElementsAre( + "https://url.test/ execution of `generateBid` timed out.")); + + // Second run should have the same behavior, not different one due to + // wrong timing of utask execution. + GenerateBid(bidder_worklet.get()); + generate_bid_run_loop_ = std::make_unique<base::RunLoop>(); + generate_bid_run_loop_->Run(); + EXPECT_EQ(0u, bids_.size()); + EXPECT_THAT(bid_errors_, + testing::ElementsAre( + "https://url.test/ execution of `generateBid` timed out.")); +} + class BidderWorkletRealTimeReportingEnabledTest : public BidderWorkletTest { public: BidderWorkletRealTimeReportingEnabledTest() { diff --git a/content/services/auction_worklet/context_recycler.cc b/content/services/auction_worklet/context_recycler.cc index 215c36a..4036d8cf7 100644 --- a/content/services/auction_worklet/context_recycler.cc +++ b/content/services/auction_worklet/context_recycler.cc @@ -183,6 +183,12 @@ } void ContextRecycler::ResetForReuse() { + // Make sure that microtasks get flushed as they would not on timeout. + { + AuctionV8Helper::TimeLimitScope time_scope(v8_helper_->GetTimeLimit()); + v8_helper_->isolate()->PerformMicrotaskCheckpoint(); + } + for (Bindings* bindings : bindings_list_) { bindings->Reset(); }
Regression Test / PoC
diff --git a/content/services/auction_worklet/bidder_worklet_unittest.cc b/content/services/auction_worklet/bidder_worklet_unittest.cc
index 743b2bde..5805ba89 100644
--- a/content/services/auction_worklet/bidder_worklet_unittest.cc
+++ b/content/services/auction_worklet/bidder_worklet_unittest.cc
@@ -15091,6 +15091,41 @@
BidderWorklet::SignalsOriginRelation::kCrossOriginSignals, 5);
}
+TEST_F(BidderWorkletTest, MicroTaskTiming) {
+ const char kScript[] = R"(
+ function generateBid() {
+ Promise.resolve().then(() => {
+ /* If this runs at wrong time, the error will be top-level timeout,
+ * not generateBid one */
+ while(true) {}
+ });
+
+ while(true) {};
+ }
+ )";
+
+ mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
+ AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
+ kScript);
+ GenerateBid(bidder_worklet.get());
+ generate_bid_run_loop_ = std::make_unique<base::RunLoop>();
+ generate_bid_run_loop_->Run();
+ EXPECT_EQ(0u, bids_.size());
+ EXPECT_THAT(bid_errors_,
+ testing::ElementsAre(
+ "https://url.test/ execution of `generateBid` timed out."));
+
+ // Second run should have the same behavior, not different one due to
+ // wrong timing of utask execution.
+ GenerateBid(bidder_worklet.get());
+ generate_bid_run_loop_ = std::make_unique<base::RunLoop>();
+ generate_bid_run_loop_->Run();
+ EXPECT_EQ(0u, bids_.size());
+ EXPECT_THAT(bid_errors_,
+ testing::ElementsAre(
+ "https://url.test/ execution of `generateBid` timed out."));
+}
+
class BidderWorkletRealTimeReportingEnabledTest : public BidderWorkletTest {
public:
BidderWorkletRealTimeReportingEnabledTest() {
Original Bug Report
Heap Use-After-Free and Stack UAR in Protected Audience Worklets via Stranded Microtasks
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free and Stack Use-After-Return in Protected Audience (FLEDGE) worklets is caused by a failure to isolate and drain V8 microtask queues during execution termination. When the execution timeout watchdog terminates script execution, pending Promise microtasks can remain stranded in the shared default microtask queue. Subsequent execution cycles on the same V8 thread can trigger these stranded microtasks, causing callbacks to dereference deallocated binding objects or dead stack frames.
Affected files:
content/services/auction_worklet/auction_v8_helper.cccontent/services/auction_worklet/report_bindings.cccontent/services/auction_worklet/bidder_worklet.cccontent/services/auction_worklet/seller_worklet.cccontent/services/auction_worklet/lazy_filler.cccontent/services/auction_worklet/deprecated_url_lazy_filler.cc
Estimated timestamp from git blame: 2021-04-13
Detailed Analysis of the Potential Vulnerability
There is a potential Heap Use-After-Free (UAF) and Stack Use-After-Return (UAR) vulnerability within the Protected Audience (FLEDGE) utility process. The issue stems from a combination of sharing V8’s default microtask queue, execution termination handling, and a lack of lifetime verification in the binding wrappers.
1. Shared Default Microtask Queue
In AuctionV8Helper::CreateContext (located in content/services/auction_worklet/auction_v8_helper.cc at line 381), the V8 context is created without specifying a dedicated microtask queue:
v8::Local<v8::Context> context =
v8::Context::New(isolate(), /*extensions=*/nullptr, global_template);
Because the microtask_queue parameter defaults to nullptr, all worklet contexts created on the same thread share the V8 isolate’s single default microtask queue.
2. Execution Termination and Stranded Microtasks
When a worklet script runs, it may schedule microtasks (e.g., via a resolved Promise callback) which are added to the default microtask queue. If the script subsequently runs into an infinite loop or takes too long, the background watchdog timer fires and calls v8::Isolate::TerminateExecution() (defined in content/services/auction_worklet/auction_v8_helper.cc at line 247).
During stack unwinding, V8 reaches the microtask checkpoint helper Isolate::FireCallCompletedCallbackInternal(). Because is_execution_terminating() returns true, the microtask execution checkpoint is skipped:
bool perform_checkpoint = microtask_queue && !is_execution_terminating(); // Evaluates to false; checkpoint is skipped
After returning from execution, CancelTerminateExecution() is called to restore the isolate’s capability to run scripts. However, any unexecuted microtasks remain stranded in the shared default microtask queue. Since these microtasks retain strong references to the V8 context and global proxy, they prevent V8 from garbage collecting the context immediately.
3. Lifetime Mismatch (UAF/UAR)
When execution ends, the stack frame unwinds and the ContextRecycler (often allocated on the stack, e.g., in BidderWorklet::V8State::ReportWin at bidder_worklet.cc:1133) is destroyed, which deallocates any associated bindings, such as ReportBindings. Similarly, stack-allocated objects such as DeprecatedUrlLazyFiller are popped off the stack and destroyed.
If a subsequent execution runs on the same V8 isolate and completes successfully, V8 triggers a microtask checkpoint on the default queue because is_execution_terminating() is now false. This executes the stranded microtask from the previous run. The microtask attempts to execute its callback (e.g., sendReportTo), which invokes ReportBindings::SendReportTo (defined in content/services/auction_worklet/report_bindings.cc at line 71):
ReportBindings* bindings = static_cast<ReportBindings*>(
v8::External::Cast(*args.Data())->Value(gin::kReportBindingsTag));
Because the ContextRecycler and its owned ReportBindings have already been destroyed, this retrieves a dangling pointer. Attempting to access its fields (like bindings->v8_helper_) results in a Use-After-Free. A similar path via stack-allocated DeprecatedUrlLazyFiller results in a Use-After-Return (UAR).
Potential Steps to Trigger the Vulnerability
Note: These are theoretical/potential steps identified during static analysis; our tooling agent does not have the capability to run code or execute a live proof-of-concept.
- Register two sequential interest groups to run auctions on the same V8 utility thread.
- In the first interest group’s bidding script, schedule a microtask via a resolved Promise callback that invokes a binding function (e.g.,
sendReportTo), followed immediately by an infinite loop (for(;;);) to trigger the 50 ms watchdog timeout. - The script will be aborted due to execution termination, leaving the pending microtask stranded in the shared default microtask queue.
- The stack unwinds, and the stack-allocated
ContextRecyclerandReportBindingsare destroyed. - When the second interest group’s script executes and completes, V8 pumps the default microtask queue, running the stranded callback with a dangling pointer and causing an ASan violation.
Suggested Remediation
To prevent microtasks from one context execution leaking into and running during subsequent execution cycles, V8 context microtask queues should be isolated per context.
Recommended Fix:
Create an independent, isolated v8::MicrotaskQueue for each v8::Context created inside AuctionV8Helper::CreateContext(). This can be achieved by allocating a v8::MicrotaskQueue instance and passing it to v8::Context::New():
std::unique_ptr<v8::MicrotaskQueue> microtask_queue = v8::MicrotaskQueue::New(isolate());
v8::Local<v8::Context> context = v8::Context::New(
isolate(), /*extensions=*/nullptr, global_template,
/*global_object=*/v8::MaybeLocal<v8::Value>(),
/*internal_fields_deserializer=*/v8::DeserializeInternalFieldsCallback(),
microtask_queue.get());
Managing the lifecycle of the MicrotaskQueue alongside the context ensures that any pending or stranded microtasks are immediately discarded when the queue and context are destroyed, rather than leaking into the shared isolate queue.
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.