CVE-2026-13815
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/ad_tracker/ad_tracker.cc |
modified |
Files Changed
third_party/blink/renderer/core/ad_tracker/ad_tracker.ccthird_party/blink/renderer/core/ad_tracker/ad_tracker.h
Patch
From 6392d100129652e91df0505c5eb39e305e9142e5 Mon Sep 17 00:00:00 2001 From: Josh Karlin <[email protected]> Date: Mon, 11 May 2026 07:40:20 -0700 Subject: [PATCH] [AdTracker] Fix potentially invalid iterator access The AdTracker held onto an iterator across a js call, which could have invalidated the iterator. Fix by holding onto an ad script identifier instead of an iterator. Bug: 511722207 Change-Id: I94f7c06696fb386e3e4d66a6a628e6de07ff7a74 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7836598 Auto-Submit: Josh Karlin <[email protected]> Reviewed-by: Yao Xiao <[email protected]> Commit-Queue: Yao Xiao <[email protected]> Cr-Commit-Position: refs/heads/main@{#1628561} --- diff --git a/third_party/blink/renderer/core/ad_tracker/ad_tracker.cc b/third_party/blink/renderer/core/ad_tracker/ad_tracker.cc index b0a1014..9817f64d 100644 --- a/third_party/blink/renderer/core/ad_tracker/ad_tracker.cc +++ b/third_party/blink/renderer/core/ad_tracker/ad_tracker.cc @@ -503,7 +503,7 @@ return false; } - auto ad_script_it = ad_script_data_.end(); + std::optional<AdScriptIdentifier> matched_ad_script; int ad_script_index = -1; for (size_t i = 0; i < stack.size(); ++i) { @@ -515,12 +515,12 @@ auto it = ad_script_data_.find(script_id); if (it != ad_script_data_.end()) { ad_script_index = static_cast<int>(i); - ad_script_it = it; + matched_ad_script = it->value.id; break; } } - if (ad_script_it == ad_script_data_.end()) { + if (!matched_ad_script.has_value()) { // The top scripts on the stack are not registered ad script. Are they // from ad frames? @@ -538,7 +538,7 @@ IsFunctionAMonkeyPatch(isolate, stack[ad_script_index - 1].function, ignore_monkey_patch)) { if (out_ad_script) { - *out_ad_script = ad_script_it->value.id; + *out_ad_script = *matched_ad_script; } return true; } @@ -558,7 +558,7 @@ } if (out_ad_script) { - *out_ad_script = ad_script_it->value.id; + *out_ad_script = *matched_ad_script; } return true; diff --git a/third_party/blink/renderer/core/ad_tracker/ad_tracker.h b/third_party/blink/renderer/core/ad_tracker/ad_tracker.h index 24b31e82..e342fde 100644 --- a/third_party/blink/renderer/core/ad_tracker/ad_tracker.h +++ b/third_party/blink/renderer/core/ad_tracker/ad_tracker.h @@ -212,6 +212,9 @@ // Returns true if `api` is a monkeyaptched function and matches `function` in // the `isolate`'s current context. + // WARNING: This function executes js and can therefore modify the members of + // this class. Consider all iterators obtained before calling + // IsFunctionAMonkeyPatch to be invalid. // TODO(jkarlin): This function really wants a context, not an isolate. bool IsFunctionAMonkeyPatch(v8::Isolate* isolate, const v8::Local<v8::Function>& function,
Original Bug Report
UAF in AdTracker::IsAdScriptInStackHelper via V8 getter reentrancy
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 without the Chrome Security team. 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 exists in AdTracker::IsAdScriptInStackHelper because it holds a raw iterator to a WTF::HashMap across a synchronous JavaScript call. An attacker can use a property getter to modify the map and trigger a rehash, invalidating the iterator. Dereferencing the dangling iterator allows the attacker to manipulate reference counts, leading to an arbitrary free primitive.
Affected files:
third_party/blink/renderer/core/ad_tracker/ad_tracker.ccthird_party/blink/renderer/core/ad_tracker/ad_tracker.hthird_party/blink/renderer/bindings/core/v8/js_event_handler_for_content_attribute.cc
Estimated timestamp from git blame: 2026-03-03
Summary
A potential Use-After-Free (UAF) vulnerability exists in AdTracker::IsAdScriptInStackHelper within Blink. The function holds a raw iterator to ad_script_data_ (a WTF::HashMap) while executing a code path that allows synchronous JavaScript execution. A malicious page can exploit this reentrancy to force a rehash of the HashMap, freeing its backing store. Upon returning to C++, the dangling iterator is dereferenced, leading to attacker-controlled memory corruption and a potential arbitrary free primitive in the renderer process.
Vulnerability Details
In third_party/blink/renderer/core/ad_tracker/ad_tracker.cc, IsAdScriptInStackHelper captures an iterator (ad_script_it) into the ad_script_data_ map to track whether the currently executing script is an ad script:
auto it = ad_script_data_.find(script_id);
if (it != ad_script_data_.end()) {
ad_script_index = static_cast<int>(i);
ad_script_it = it;
break;
}
Later in the function, it attempts to determine if an API was called via monkey-patching by calling IsFirstCallOfApiFromNonAdScript -> WasApiCalledByNonAdScript -> GetApiFunctionInfo.
GetApiFunctionInfo attempts to resolve a property path on the global object, such as window.Node.prototype.appendChild. Because properties on the global object (like Node) are configurable, an attacker can define a synchronous JavaScript getter for window.Node.
During this reentrant execution, the attacker has full control. They can trigger operations that modify the ad_script_data_ map. Specifically, reading the onclick attribute of DOM elements that were previously created by an ad script will trigger JSEventHandlerForContentAttribute::GetCompiledHandler. This lazily compiles the script and calls AdTracker::RegisterAdScript, inserting a new entry into ad_script_data_.
By doing this in a loop, the attacker can force the HashMap to expand. Expanding the map allocates a new backing store and frees the old one via PartitionAlloc.
When the malicious getter returns, C++ execution resumes. The ad_script_it iterator is now a dangling pointer pointing into the freed backing store. The code subsequently dereferences it:
if (out_ad_script) {
*out_ad_script = ad_script_it->value.id;
}
This assigns an AdScriptIdentifier, which contains a WTF::String. Copying a WTF::String copies a scoped_refptr<StringImpl>, which reads the underlying StringImpl* pointer from the attacker-controlled freed memory and calls AddRef() on it. When the local out_ad_script variable is destroyed, it calls Release() on that same pointer.
By spraying the heap during the reentrant call, an attacker can control the contents of the freed memory. Supplying a fake StringImpl object with a reference count of 1 allows the attacker to trigger an arbitrary delete (free) when Release() drops the count to 0.
Note: This issue is not mitigated by MiraclePtr (BackupRefPtr) because the dangling pointer is held inside a stack-allocated iterator (WTF::HashTableConstIterator), which is explicitly excluded from BRP protection.
Potential Reproduction Steps
(Note: These are suggested steps to trigger the vulnerability; a working PoC has not yet been executed.)
- Serve a page that loads a script matching the ad ruleset so it is registered in
ad_script_data_. - From the ad script, create multiple detached elements and set their
onclickattributes. - Define a malicious getter for
window.Nodeon the global object. - Trigger a synchronous subresource fetch (e.g.,
new Image().src = '...'). This synchronously reachesAdTracker::CalculateIfAdSubresource->IsAdScriptInStackHelper. IsAdScriptInStackHelpercaptures the iterator and triggers theNodegetter.- Inside the getter, loop through the detached elements and read their
onclickproperties to triggerRegisterAdScriptuntilad_script_data_rehashes. - Allocate objects to spray the freed PartitionAlloc buffer with a fake
StringImplpayload. - Return from the getter. The dangling iterator is dereferenced, executing an arbitrary free on the attacker-controlled address.
Suggested Fix
Do not hold the ad_script_it iterator across the boundary where JavaScript can execute. Instead of storing the iterator, store the script_id and perform the ad_script_data_.find(script_id) lookup again after the potentially re-entrant calls (IsFirstCallOfApiFromNonAdScript / IsFunctionAMonkeyPatch) have completed.
Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.