CVE-2026-15133
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/services/auction_worklet/bidder_worklet.cc |
modified | |
TEST_Fcontent/services/auction_worklet/bidder_worklet_unittest.cc |
modified | |
whilecontent/services/auction_worklet/bidder_worklet_unittest.cc |
modified | |
forcontent/services/auction_worklet/context_recycler.cc |
modified | |
warning_content/services/auction_worklet/deprecated_url_lazy_filler.cc |
modified | |
ifcontent/services/auction_worklet/deprecated_url_lazy_filler.cc |
modified |
Files Changed
content/services/auction_worklet/bidder_worklet.cccontent/services/auction_worklet/bidder_worklet_unittest.cccontent/services/auction_worklet/context_recycler.cccontent/services/auction_worklet/deprecated_url_lazy_filler.cccontent/services/auction_worklet/deprecated_url_lazy_filler.h
Patch
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,
Regression Test / PoC
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.
//
Original Bug Report
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-25documents the lifetime invariant: the associated V8 context must be destroyed immediately after theLazyFillerto avoid UAF.content/services/auction_worklet/lazy_filler.cc:38-43stores rawthisin V8 callback data withv8::External::New(isolate, this, tag_).content/services/auction_worklet/lazy_filler.h:55-57recovers the raw native pointer from the V8 callback data.content/services/auction_worklet/deprecated_url_lazy_filler.cc:38-48uses the recoveredDeprecatedUrlLazyFiller*and dereferencesself->v8_logger_,self->warning_, andself->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:1135declaresContextRecyclerScope context_recycler_scope(context_recycler);.content/services/auction_worklet/bidder_worklet.cc:1179-1182later declares stack objectDeprecatedUrlLazyFiller deprecated_render_url(...).content/services/auction_worklet/bidder_worklet.cc:1187-1199passes&deprecated_render_urlintoSetBrowserSignals(), which installs the deprecatedbrowserSignals.renderUrllazy accessor.content/services/auction_worklet/bidder_worklet.cc:1295-1308returns on non-success, includingResult::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-190callsv8_helper_->isolate()->PerformMicrotaskCheckpoint().content/services/auction_worklet/context_recycler.cc:217-219callsResetForReuse()from theContextRecyclerScopedestructor.
This creates the bad ordering:
reportWin()receivesbrowserSignals.- JavaScript saves
browserSignalsand queues a Promise microtask that reads deprecatedbrowserSignals.renderUrl. reportWin()times out, so normal script completion does not run the microtask before stack unwind.BidderWorklet::V8State::ReportWin()unwinds and destroys stackDeprecatedUrlLazyFiller deprecated_render_url.ContextRecyclerScopeis destroyed after that and runsResetForReuse().ResetForReuse()performs a V8 microtask checkpoint while the V8 context still contains the savedbrowserSignalsobject.- The queued microtask reads
browserSignals.renderUrl, invokingDeprecatedUrlLazyFiller::HandleDeprecatedUrl()with callback data pointing to the already-destroyed stack object. - The getter dereferences the dangling
DeprecatedUrlLazyFiller*, causing ASanstack-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.
- 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.
- 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
Is the security issue related to a crash?
Yes, it is related to a crash.
Choose the type of vulnerability
Memory Corruption