Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in InterestGroups
DescriptionUse after free in InterestGroups
ComponentInterestGroups
Bug ClassUAF
Tracker516902973
Fix commit5cfe401fdda6 (chromium/src) +41/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
TEST_F
content/services/auction_worklet/bidder_worklet_unittest.cc
modified
while
content/services/auction_worklet/bidder_worklet_unittest.cc
modified
BidderWorkletRealTimeReportingEnabledTest
content/services/auction_worklet/bidder_worklet_unittest.cc
modified
BidderWorkletRealTimeReportingEnabledTest
content/services/auction_worklet/bidder_worklet_unittest.cc
modified
for
content/services/auction_worklet/context_recycler.cc
modified

Files Changed

  • content/services/auction_worklet/bidder_worklet_unittest.cc
  • content/services/auction_worklet/context_recycler.cc
From 5cfe401fdda609ebf3333c2733b78e3e585207fe Mon Sep 17 00:00:00 2001
From: Maks Orlovich <morlovich@chromium.org>
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 <morlovich@chromium.org>
Reviewed-by: Russ Hamilton <behamilton@google.com>
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();
   }
Loading diff…

Regression Test / PoC

shipped with the fix
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() {
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • content/services/auction_worklet/report_bindings.cc
  • content/services/auction_worklet/bidder_worklet.cc
  • content/services/auction_worklet/seller_worklet.cc
  • content/services/auction_worklet/lazy_filler.cc
  • content/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.

  1. Register two sequential interest groups to run auctions on the same V8 utility thread.
  2. 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.
  3. The script will be aborted due to execution termination, leaving the pending microtask stranded in the shared default microtask queue.
  4. The stack unwinds, and the stack-allocated ContextRecycler and ReportBindings are destroyed.
  5. 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.

View on issue tracker