Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Blink
DescriptionUse after free in Blink
ComponentBlink
Bug ClassUAF
Tracker511722207
Fix commit6392d1001296 (chromium/src) +8/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/ad_tracker/ad_tracker.cc
modified

Files Changed

  • third_party/blink/renderer/core/ad_tracker/ad_tracker.cc
  • third_party/blink/renderer/core/ad_tracker/ad_tracker.h
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,
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_party/blink/renderer/core/ad_tracker/ad_tracker.h
  • third_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.)

  1. Serve a page that loads a script matching the ad ruleset so it is registered in ad_script_data_.
  2. From the ad script, create multiple detached elements and set their onclick attributes.
  3. Define a malicious getter for window.Node on the global object.
  4. Trigger a synchronous subresource fetch (e.g., new Image().src = '...'). This synchronously reaches AdTracker::CalculateIfAdSubresource -> IsAdScriptInStackHelper.
  5. IsAdScriptInStackHelper captures the iterator and triggers the Node getter.
  6. Inside the getter, loop through the detached elements and read their onclick properties to trigger RegisterAdScript until ad_script_data_ rehashes.
  7. Allocate objects to spray the freed PartitionAlloc buffer with a fake StringImpl payload.
  8. 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.

View on issue tracker