Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in InterestGroups
DescriptionUse after free in InterestGroups
ComponentInterestGroups
Bug ClassUAF
Tracker527406824
Fix commit5a68bc6c97d9 (chromium/src) +65/-29
CISA KEVNot listed
CreditedJihyeon Jeong (Compsec Lab, Seoul National University / Research Intern)
Disclosed2026-07-08

Changed Functions

FunctionChangeNotes
if
content/services/auction_worklet/bidder_worklet.cc
modified
TEST_F
content/services/auction_worklet/bidder_worklet_unittest.cc
modified
while
content/services/auction_worklet/bidder_worklet_unittest.cc
modified
for
content/services/auction_worklet/context_recycler.cc
modified
warning_
content/services/auction_worklet/deprecated_url_lazy_filler.cc
modified
if
content/services/auction_worklet/deprecated_url_lazy_filler.cc
modified

Files Changed

  • content/services/auction_worklet/bidder_worklet.cc
  • content/services/auction_worklet/bidder_worklet_unittest.cc
  • content/services/auction_worklet/context_recycler.cc
  • content/services/auction_worklet/deprecated_url_lazy_filler.cc
  • content/services/auction_worklet/deprecated_url_lazy_filler.h
From 5a68bc6c97d97312ffa0f9ac04e2288f1cdadd13 Mon Sep 17 00:00:00 2001
From: Maks Orlovich <morlovich@chromium.org>
Date: Fri, 26 Jun 2026 15:18:17 -0700
Subject: [PATCH] FLEDGE: fix lifetime reportWin's deprecatedUrl handler.

Other lazy fillers seem fine on read-through, but just in case:
1) Reuse unused logger param from the config filler --- it gets
   it from ContextRecycler, and using the passed in one would
   cause trouble.
2) Call the checkpoint after resetting the bindings and lazy fillers.

Bug: 527406824
Change-Id: I570a1267be21d9d398c4e2102e21a6bb923345c2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8006698
Reviewed-by: mmenke <mmenke@chromium.org>
Commit-Queue: Maks Orlovich <morlovich@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1653488}
---

diff --git a/content/services/auction_worklet/bidder_worklet.cc b/content/services/auction_worklet/bidder_worklet.cc
index 53a0ce1eb..489e1fc 100644
--- a/content/services/auction_worklet/bidder_worklet.cc
+++ b/content/services/auction_worklet/bidder_worklet.cc
@@ -1128,6 +1128,12 @@
   AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper_.get());
   v8::Isolate* isolate = v8_helper_->isolate();
 
+  // Needs to outlast ContextRecyclerScope. See crbug.com/527406824.
+  DeprecatedUrlLazyFiller deprecated_render_url(
+      v8_helper_.get(), &browser_signal_render_url,
+      "browserSignals.renderUrl is deprecated."
+      " Please use browserSignals.renderURL instead.");
+
   // Short lived context, to avoid leaking data at global scope between either
   // repeated calls to this worklet, or to calls to any other worklet.
   ContextRecycler context_recycler(v8_helper_.get());
@@ -1176,10 +1182,6 @@
 
   context_recycler.AddReportWinBrowserSignalsLazyFiller();
 
-  DeprecatedUrlLazyFiller deprecated_render_url(
-      v8_helper_.get(), &v8_logger, &browser_signal_render_url,
-      "browserSignals.renderUrl is deprecated."
-      " Please use browserSignals.renderURL instead.");
   base::TimeDelta reporting_timeout =
       browser_signal_reporting_timeout.has_value()
           ? *browser_signal_reporting_timeout
@@ -1238,6 +1240,8 @@
   }
   args.push_back(direct_from_seller_signals);
 
+  deprecated_render_url.SetLogger(&v8_logger);
+
   // An empty return value indicates an exception was thrown. Any other return
   // value indicates no exception.
   v8_helper_->MaybeTriggerInstrumentationBreakpoint(
@@ -1253,6 +1257,7 @@
       v8_helper_->RunScript(context, unbound_worklet_script, debug_id_.get(),
                             total_timeout.get(), errors_out);
   if (result != AuctionV8Helper::Result::kSuccess) {
+    deprecated_render_url.SetLogger(nullptr);
     TRACE_EVENT_END("fledge", perfetto::Track(trace_id));
     PostReportWinCallbackToUserThread(
         std::move(callback), /*report_url=*/std::nullopt,
@@ -1293,6 +1298,7 @@
   base::UmaHistogramTimes("Ads.InterestGroup.Auction.ReportWinTime", elapsed);
 
   if (result != AuctionV8Helper::Result::kSuccess) {
+    deprecated_render_url.SetLogger(nullptr);
     // Keep Private Aggregation API requests since `reportWin()` might use it to
     // detect script timeout or failures.
     PostReportWinCallbackToUserThread(
@@ -1399,6 +1405,7 @@
   }
   // This covers both the case where a report URL was provided, and the case one
   // was not.
+  deprecated_render_url.SetLogger(nullptr);
   PostReportWinCallbackToUserThread(
       std::move(callback), context_recycler.report_bindings()->report_url(),
       context_recycler.register_ad_beacon_bindings()->TakeAdBeaconMap(),
diff --git a/content/services/auction_worklet/bidder_worklet_unittest.cc b/content/services/auction_worklet/bidder_worklet_unittest.cc
index 5805ba89..a6542e1 100644
--- a/content/services/auction_worklet/bidder_worklet_unittest.cc
+++ b/content/services/auction_worklet/bidder_worklet_unittest.cc
@@ -8712,6 +8712,33 @@
   channel->ExpectNoMoreConsoleEvents();
 }
 
+TEST_F(BidderWorkletTest, ReportWinBrowserSignalRenderUrlDeprecationAsync) {
+  // From https://crbug.com/527406824
+  const char kBody[] = R"(
+      globalThis.savedBrowserSignals = browserSignals;
+      Promise.resolve().then(() => {
+        void globalThis.savedBrowserSignals.renderUrl;
+      });
+
+      // Force reportWin timeout. V8's normal auto microtask checkpoint is
+      // skipped while execution is terminating; AuctionWorklet's
+      // ContextRecyclerScope then performs a later ResetForReuse() microtask
+      // checkpoint after stack unwind.
+      while (true) {}
+  )";
+  AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
+                        CreateReportWinScript(kBody));
+  RunReportWinExpectingResult(
+      /*expected_report_url=*/std::nullopt,
+      /*expected_ad_beacon_map=*/{},
+      /*expected_ad_macro_map=*/{},
+      /*expected_pa_requests=*/{},
+      /*expected_pmt_request_data=*/nullptr,
+      /*expected_reporting_latency_timeout=*/true,
+      /*expected_errors=*/
+      {"https://url.test/ execution of `reportWin` timed out."});
+}
+
 // Check that accessing `renderURL` of browserSignals does not display a
 // warning.
 //
diff --git a/content/services/auction_worklet/context_recycler.cc b/content/services/auction_worklet/context_recycler.cc
index 4036d8cf7..38e26c5 100644
--- a/content/services/auction_worklet/context_recycler.cc
+++ b/content/services/auction_worklet/context_recycler.cc
@@ -183,12 +183,6 @@
 }
 
 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();
   }
@@ -207,6 +201,12 @@
   for (const auto& auction_config_lazy_filler : auction_config_lazy_fillers_) {
     auction_config_lazy_filler->Reset();
   }
+
+  // Make sure that microtasks get flushed as they would not on timeout.
+  {
+    AuctionV8Helper::TimeLimitScope time_scope(v8_helper_->GetTimeLimit());
+    v8_helper_->isolate()->PerformMicrotaskCheckpoint();
+  }
 }
 
 ContextRecyclerScope::ContextRecyclerScope(ContextRecycler& context_recycler)
diff --git a/content/services/auction_worklet/deprecated_url_lazy_filler.cc b/content/services/auction_worklet/deprecated_url_lazy_filler.cc
index 22c65d0f..9dd7302b 100644
--- a/content/services/auction_worklet/deprecated_url_lazy_filler.cc
+++ b/content/services/auction_worklet/deprecated_url_lazy_filler.cc
@@ -6,6 +6,7 @@
 
 #include <string_view>
 
+#include "base/check.h"
 #include "base/memory/raw_ptr.h"
 #include "content/services/auction_worklet/auction_v8_helper.h"
 #include "content/services/auction_worklet/auction_v8_logger.h"
@@ -17,15 +18,16 @@
 namespace auction_worklet {
 
 DeprecatedUrlLazyFiller::DeprecatedUrlLazyFiller(AuctionV8Helper* v8_helper,
-                                                 AuctionV8Logger* v8_logger,
                                                  const GURL* url,
                                                  const char* warning)
     : LazyFiller(v8_helper, kTag),
-      v8_logger_(v8_logger),
+      v8_logger_(nullptr),
       url_(url),
       warning_(warning) {}
 
-DeprecatedUrlLazyFiller::~DeprecatedUrlLazyFiller() = default;
+DeprecatedUrlLazyFiller::~DeprecatedUrlLazyFiller() {
+  DCHECK_EQ(v8_logger_, nullptr);
+}
 
 bool DeprecatedUrlLazyFiller::AddDeprecatedUrlGetter(
     v8::Local<v8::Object> object,
@@ -39,7 +41,9 @@
     v8::Local<v8::Name> name,
     const v8::PropertyCallbackInfo<v8::Value>& info) {
   DeprecatedUrlLazyFiller* self = GetSelf<DeprecatedUrlLazyFiller>(info);
-  self->v8_logger_->LogConsoleWarning(self->warning_.get());
+  if (self->v8_logger_) {
+    self->v8_logger_->LogConsoleWarning(self->warning_.get());
+  }
 
   AuctionV8Helper* v8_helper = self->v8_helper();
   v8::Isolate* isolate = v8_helper->isolate();
diff --git a/content/services/auction_worklet/deprecated_url_lazy_filler.h b/content/services/auction_worklet/deprecated_url_lazy_filler.h
index 0a23197e..eaf9a82 100644
--- a/content/services/auction_worklet/deprecated_url_lazy_filler.h
+++ b/content/services/auction_worklet/deprecated_url_lazy_filler.h
@@ -34,12 +34,14 @@
   // DeprecatedUrlLazyFiller. Additionally, `url` and `warning` must not be
   // modified until the DeprecatedUrlLazyFiller is destroyed.
   DeprecatedUrlLazyFiller(AuctionV8Helper* v8_helper,
-                          AuctionV8Logger* v8_logger,
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 5805ba89..a6542e1 100644
--- a/content/services/auction_worklet/bidder_worklet_unittest.cc
+++ b/content/services/auction_worklet/bidder_worklet_unittest.cc
@@ -8712,6 +8712,33 @@
   channel->ExpectNoMoreConsoleEvents();
 }
 
+TEST_F(BidderWorkletTest, ReportWinBrowserSignalRenderUrlDeprecationAsync) {
+  // From https://crbug.com/527406824
+  const char kBody[] = R"(
+      globalThis.savedBrowserSignals = browserSignals;
+      Promise.resolve().then(() => {
+        void globalThis.savedBrowserSignals.renderUrl;
+      });
+
+      // Force reportWin timeout. V8's normal auto microtask checkpoint is
+      // skipped while execution is terminating; AuctionWorklet's
+      // ContextRecyclerScope then performs a later ResetForReuse() microtask
+      // checkpoint after stack unwind.
+      while (true) {}
+  )";
+  AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
+                        CreateReportWinScript(kBody));
+  RunReportWinExpectingResult(
+      /*expected_report_url=*/std::nullopt,
+      /*expected_ad_beacon_map=*/{},
+      /*expected_ad_macro_map=*/{},
+      /*expected_pa_requests=*/{},
+      /*expected_pmt_request_data=*/nullptr,
+      /*expected_reporting_latency_timeout=*/true,
+      /*expected_errors=*/
+      {"https://url.test/ execution of `reportWin` timed out."});
+}
+
 // Check that accessing `renderURL` of browserSignals does not display a
 // warning.
 //
Loading diff…

Original Bug Report

reported by sm...@gmail.com

AuctionWorklet reportWin timeout triggers use-after-scope in deprecated renderUrl lazy getter


Report description

AuctionWorklet reportWin timeout triggers use-after-scope in deprecated renderUrl lazy getter


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules


The problem

Please describe the technical details of the vulnerability

This is a lifetime bug in the Protected Audience AuctionWorklet V8 boundary. A V8-visible lazy accessor stores a raw pointer to a stack-owned native object as callback data. Under a reportWin() timeout, a pending JavaScript microtask can later invoke that accessor after the native stack object has already gone out of scope, producing an ASan stack-use-after-scope in the AuctionWorklet utility process.

The vulnerable callback-data pattern is in content/services/auction_worklet/lazy_filler.cc. LazyFiller::DefineLazyAttribute() installs a V8 lazy data property and passes v8::External::New(isolate, this, tag_) as the accessor data. LazyFiller::GetSelf<T>() later recovers that pointer from info.Data().

Relevant source:

  • content/services/auction_worklet/lazy_filler.h:21-25 documents the lifetime invariant: the associated V8 context must be destroyed immediately after the LazyFiller to avoid UAF.
  • content/services/auction_worklet/lazy_filler.cc:38-43 stores raw this in V8 callback data with v8::External::New(isolate, this, tag_).
  • content/services/auction_worklet/lazy_filler.h:55-57 recovers the raw native pointer from the V8 callback data.
  • content/services/auction_worklet/deprecated_url_lazy_filler.cc:38-48 uses the recovered DeprecatedUrlLazyFiller* and dereferences self->v8_logger_, self->warning_, and self->url_.

The affected accessor is the deprecated lowercase-camelcase alias browserSignals.renderUrl in bidder reportWin() browser signals. The current field is browserSignals.renderURL; the deprecated alias is implemented through DeprecatedUrlLazyFiller so it can warn and lazily return the same URL. Accessing browserSignals.renderURL does not hit this DeprecatedUrlLazyFiller path, but accessing browserSignals.renderUrl does.

In BidderWorklet::V8State::ReportWin() the local lifetime order is unsafe on the timeout path:

  • content/services/auction_worklet/bidder_worklet.cc:1135 declares ContextRecyclerScope context_recycler_scope(context_recycler);.
  • content/services/auction_worklet/bidder_worklet.cc:1179-1182 later declares stack object DeprecatedUrlLazyFiller deprecated_render_url(...).
  • content/services/auction_worklet/bidder_worklet.cc:1187-1199 passes &deprecated_render_url into SetBrowserSignals(), which installs the deprecated browserSignals.renderUrl lazy accessor.
  • content/services/auction_worklet/bidder_worklet.cc:1295-1308 returns on non-success, including Result::kTimeout.

Because C++ destroys local variables in reverse declaration order, deprecated_render_url is destroyed before context_recycler_scope during function unwind. However, ContextRecyclerScope::~ContextRecyclerScope() calls ContextRecycler::ResetForReuse():

  • content/services/auction_worklet/context_recycler.cc:185-190 calls v8_helper_->isolate()->PerformMicrotaskCheckpoint().
  • content/services/auction_worklet/context_recycler.cc:217-219 calls ResetForReuse() from the ContextRecyclerScope destructor.

This creates the bad ordering:

  1. reportWin() receives browserSignals.
  2. JavaScript saves browserSignals and queues a Promise microtask that reads deprecated browserSignals.renderUrl.
  3. reportWin() times out, so normal script completion does not run the microtask before stack unwind.
  4. BidderWorklet::V8State::ReportWin() unwinds and destroys stack DeprecatedUrlLazyFiller deprecated_render_url.
  5. ContextRecyclerScope is destroyed after that and runs ResetForReuse().
  6. ResetForReuse() performs a V8 microtask checkpoint while the V8 context still contains the saved browserSignals object.
  7. The queued microtask reads browserSignals.renderUrl, invoking DeprecatedUrlLazyFiller::HandleDeprecatedUrl() with callback data pointing to the already-destroyed stack object.
  8. The getter dereferences the dangling DeprecatedUrlLazyFiller*, causing ASan stack-use-after-scope.

Steps to reproduce

The attached PoC consists of server.py and poc.html. server.py is the file to run; it serves the top-level page plus the bidder, seller, ad, trusted-bidding-signals, and reporting endpoints required by the Protected Audience flow.

  1. Start the local HTTPS PoC server:
python3 server.py --host 127.0.0.1 --port 8443

The server prints the URL to open, for example:

https://127.0.0.1:8443/

If cert.pem and key.pem are not present, the server creates a short-lived self-signed certificate with openssl.

  1. In another terminal, run an ASan Chrome build against the printed URL:
rm -rf /tmp/pa-uaf-profile
mkdir -p /tmp/pa-uaf-profile/Default
cat > /tmp/pa-uaf-profile/Default/Preferences <<'JSON'
{
  "privacy_sandbox": {
    "m1": {
      "fledge_enabled": true
    }
  }
}
/path/to/chrome \
  --headless=new \
  --user-data-dir=/tmp/pa-uaf-profile \
  --ignore-certificate-errors \
  --disable-features=EnforcePrivacySandboxAttestations,PrivacySandboxAdPrivacyUxDeprecation \
  'https://127.0.0.1:8443/'

The profile pre-seed is required for this headless local ASan reproduction because a clean Chrome profile has privacy_sandbox.m1.fledge_enabled defaulting to false; without that pref, the page does not reach the AuctionWorklet reportWin() path. Instead of editing the Preferences file, the same browser-side setting can also be enabled interactively through chrome://settings/adPrivacy/sites (Ad privacy -> Site-suggested ads) on a non-headless profile.

The remaining Chrome flags are intentionally minimal for this local repro: --headless=new for automation, --user-data-dir for the pre-seeded profile, --ignore-certificate-errors for the self-signed local HTTPS server, and --disable-features=EnforcePrivacySandboxAttestations,PrivacySandboxAdPrivacyUxDeprecation to avoid local-origin enrollment blocking and startup clearing of the pre-seeded Site-suggested ads pref. I verified that no ASAN_OPTIONS and no --enable-privacy-sandbox-ads-apis switch are required.

Bisect

I narrowed the regression to the following Chromium commit:

Gerrit CL: https://chromium-review.googlesource.com/c/chromium/src/+/7877578

Commit: 5cfe401fdda609ebf3333c2733b78e3e585207fe (FLEDGE: Fix wrong timing of microtask checkpoint execution)

The commit adds a microtask checkpoint to 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();
}

This matches the source-level root cause: the newly added timeout cleanup checkpoint can run a queued Promise microtask after BidderWorklet::V8State::ReportWin() has already destroyed stack DeprecatedUrlLazyFiller deprecated_render_url, while the V8 browserSignals.renderUrl accessor still contains callback data pointing at that stack object.

Impact analysis

This gives a deterministic, web-triggerable memory-safety failure in the AuctionWorklet execution path. In non-ASan builds, the same stale callback-data use is undefined native behavior and depends on how the expired stack slot is reused before the delayed accessor runs. The affected state includes native object fields that are consumed by the callback, so the bug is more than a benign JavaScript exception or ordinary process termination path.


The cause

What version of Chrome have you found the security issue in?

151.0.7905.0

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption

View on issue tracker