CVE-2026-7975
Overview
Background
- `TargetHandler`
- the browser-process DevTools/CDP handler that manages auto-attach to child targets and holds pending navigation
throttles_. - `TargetAutoAttacher`
- the object whose destruction triggers
AutoAttacherDestroyed, which must clear every throttle associated with it. - `Throttle::Clear`
- resumes/removes a throttle and, per the commit message, can run synchronous callbacks during navigation resumption and removes the throttle from
throttles_. - Iterator invalidation
- mutating a container while iterating it; here both the erase and a reallocation of the underlying buffer can leave the loop referencing freed memory.
Root Cause Analysis
TargetHandler::AutoAttacherDestroyed first copied the member set into a local throttles (auto throttles = throttles_;) but then, due to a typo, iterated over the member throttles_ instead of the local copy. Inside the loop, throttle->Clear() removes the throttle from throttles_, and synchronous callbacks during navigation resumption can cause the container’s underlying buffer to be reallocated — both of which invalidate the iterators of the loop that is walking throttles_. Continuing the loop over the mutated/reallocated member container dereferences stale iterators, producing a use-after-free. The one-character fix changes the loop to iterate the stable local copy throttles, so clears and reallocations of throttles_ no longer affect the iteration. The accompanying web test also shows the functional symptom: unfixed, the loop skips the second throttle, leaving a target permanently deferred.throttles_ rather than the pre-made local copy throttles; iterating the copy decouples the loop from the mutations Clear() performs on the member.Attack Path
- Enable auto-attach via CDP
A DevTools/CDP client calls
Target.setAutoAttachwithwaitForDebuggerOnStart, so new targets are deferred by throttles registered inthrottles_. - Create multiple deferred targets
The client creates several targets (the PoC creates three), populating
throttles_with multiple pending throttles. - Trigger `AutoAttacherDestroyed`
Calling
setAutoAttachagain drivesRemoveClient→AutoAttacherDestroyed, entering the loop over the memberthrottles_. - Re-entrant mutation during iteration
throttle->Clear()removes entries and resumption callbacks reallocate the buffer, invalidating the loop’s iterators overthrottles_and leading to a use-after-free (inferred: no concrete exploitation primitive is shown by the patch).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcontent/browser/devtools/protocol/target_handler.cc |
modified |
Files Changed
content/browser/devtools/protocol/target_handler.ccthird_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF-expected.txtthird_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF.js
Audit Directions
- Copy-then-iterate-wrong-containerGrep for functions that create a local snapshot of a container but still iterate the original member; the snapshot is pointless if the loop targets the live container.
- Clear/remove during iterationAudit loops calling methods like
Clear()/Remove()that re-enter and mutate the container, especially where synchronous callbacks may reallocate the backing store. - CDP-reachable handlersReview browser-process DevTools handlers that manage per-client collections (throttles, sessions) for lifetime hazards when clients are added/removed re-entrantly.
Patch
From 7ccce10b43ab3c90a589253f0121fd6206da01e1 Mon Sep 17 00:00:00 2001 From: Danil Somsikov <[email protected]> Date: Tue, 31 Mar 2026 09:34:06 -0700 Subject: [PATCH] Fix Use-After-Free in TargetHandler::AutoAttacherDestroyed This CL fixes a Use-After-Free (UAF) vulnerability in `TargetHandler::AutoAttacherDestroyed` by correcting a typo in the range-based for loop. Previously, the loop iterated over the member container `throttles_` directly instead of the intended local copy `throttles`. When `throttle->Clear()` is called within the loop, it removes the throttle from the `throttles_` set, which invalidates the loop's iterators. Furthermore, synchronous callbacks during navigation resumption can cause the container's underlying buffer to be reallocated, leading to a UAF when the loop continues. By iterating over the local copy (`throttles`) instead of the member variable, we ensure that iterator invalidation and the resulting UAF do not occur. Fixed: 497735587 Change-Id: I15006a4467234eb9ef4946059a2625630e84e120 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7712012 Reviewed-by: Andrey Kosyakov <[email protected]> Commit-Queue: Andrey Kosyakov <[email protected]> Auto-Submit: Danil Somsikov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1607917} --- diff --git a/content/browser/devtools/protocol/target_handler.cc b/content/browser/devtools/protocol/target_handler.cc index a2aaa71e..560e717 100644 --- a/content/browser/devtools/protocol/target_handler.cc +++ b/content/browser/devtools/protocol/target_handler.cc @@ -963,7 +963,7 @@ void TargetHandler::AutoAttacherDestroyed(TargetAutoAttacher* auto_attacher) { auto throttles = throttles_; - for (Throttle* throttle : throttles_) { + for (Throttle* throttle : throttles) { if (throttle->auto_attacher() == auto_attacher) { throttle->Clear(); } diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF-expected.txt new file mode 100644 index 0000000..6074f6d --- /dev/null +++ b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF-expected.txt @@ -0,0 +1,6 @@ +Tests that iterator invalidation in AutoAttacherDestroyed is fixed. +Enabled auto attach on browser target. +Created 3 targets. Throttles are now deferred. +Called setAutoAttach again on browser target. +All targets loaded successfully. + diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF.js b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF.js new file mode 100644 index 0000000..2125e12 --- /dev/null +++ b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF.js @@ -0,0 +1,44 @@ +(async function(/** @type {import('test_runner').TestRunner} */ testRunner) { + const {page, session, dp} = await testRunner.startBlank( + `Tests that iterator invalidation in AutoAttacherDestroyed is fixed.`); + + const browserTarget = testRunner.browserP().Target; + + const loadPromises = []; + browserTarget.onAttachedToTarget(async event => { + if (event.params.targetInfo.type === 'page' && + event.params.targetInfo.url.includes('empty.html')) { + const s = new TestRunner.Session(testRunner, event.params.sessionId); + s.protocol.Page.enable(); + loadPromises.push(s.protocol.Page.onceLoadEventFired()); + } + }); + + await browserTarget.setAutoAttach( + {autoAttach: true, waitForDebuggerOnStart: true, flatten: true}); + testRunner.log('Enabled auto attach on browser target.'); + + // Create 3 new targets. They will be deferred by RequestThrottle in the + // browser target. + await dp.Target.createTarget( + {url: 'http://127.0.0.1:8000/inspector-protocol/resources/empty.html?1'}); + await dp.Target.createTarget( + {url: 'http://127.0.0.1:8000/inspector-protocol/resources/empty.html?2'}); + await dp.Target.createTarget( + {url: 'http://127.0.0.1:8000/inspector-protocol/resources/empty.html?3'}); + testRunner.log('Created 3 targets. Throttles are now deferred.'); + + // Call setAutoAttach AGAIN on the browser target. + // This will trigger RemoveClient -> AutoAttacherDestroyed on the Browser + // target's TargetAutoAttacher. If unfixed, the loop will skip the second + // throttle, leaving it deferred. + await browserTarget.setAutoAttach( + {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}); + testRunner.log('Called setAutoAttach again on browser target.'); + + // Now wait for all 3 targets to finish loading. + // If unfixed, the second target will never load and this will timeout. + await Promise.all(loadPromises); + testRunner.log('All targets loaded successfully.'); + testRunner.completeTest(); +})
Regression Test / PoC
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF-expected.txt
new file mode 100644
index 0000000..6074f6d
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF-expected.txt
@@ -0,0 +1,6 @@
+Tests that iterator invalidation in AutoAttacherDestroyed is fixed.
+Enabled auto attach on browser target.
+Created 3 targets. Throttles are now deferred.
+Called setAutoAttach again on browser target.
+All targets loaded successfully.
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF.js b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF.js
new file mode 100644
index 0000000..2125e12
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF.js
@@ -0,0 +1,44 @@
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+ const {page, session, dp} = await testRunner.startBlank(
+ `Tests that iterator invalidation in AutoAttacherDestroyed is fixed.`);
+
+ const browserTarget = testRunner.browserP().Target;
+
+ const loadPromises = [];
+ browserTarget.onAttachedToTarget(async event => {
+ if (event.params.targetInfo.type === 'page' &&
+ event.params.targetInfo.url.includes('empty.html')) {
+ const s = new TestRunner.Session(testRunner, event.params.sessionId);
+ s.protocol.Page.enable();
+ loadPromises.push(s.protocol.Page.onceLoadEventFired());
+ }
+ });
+
+ await browserTarget.setAutoAttach(
+ {autoAttach: true, waitForDebuggerOnStart: true, flatten: true});
+ testRunner.log('Enabled auto attach on browser target.');
+
+ // Create 3 new targets. They will be deferred by RequestThrottle in the
+ // browser target.
+ await dp.Target.createTarget(
+ {url: 'http://127.0.0.1:8000/inspector-protocol/resources/empty.html?1'});
+ await dp.Target.createTarget(
+ {url: 'http://127.0.0.1:8000/inspector-protocol/resources/empty.html?2'});
+ await dp.Target.createTarget(
+ {url: 'http://127.0.0.1:8000/inspector-protocol/resources/empty.html?3'});
+ testRunner.log('Created 3 targets. Throttles are now deferred.');
+
+ // Call setAutoAttach AGAIN on the browser target.
+ // This will trigger RemoveClient -> AutoAttacherDestroyed on the Browser
+ // target's TargetAutoAttacher. If unfixed, the loop will skip the second
+ // throttle, leaving it deferred.
+ await browserTarget.setAutoAttach(
+ {autoAttach: true, waitForDebuggerOnStart: false, flatten: true});
+ testRunner.log('Called setAutoAttach again on browser target.');
+
+ // Now wait for all 3 targets to finish loading.
+ // If unfixed, the second target will never load and this will timeout.
+ await Promise.all(loadPromises);
+ testRunner.log('All targets loaded successfully.');
+ testRunner.completeTest();
+})
Original Bug Report
Potential Use-After-Free in TargetHandler::AutoAttacherDestroyed
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A typographical error in TargetHandler::AutoAttacherDestroyed causes iteration over the member container throttles_ instead of a local copy. During this loop, elements are erased, and synchronous callbacks can cause the container’s backing buffer to be reallocated, leading to a Use-After-Free (UAF) of the container’s buffer and potential Remote Code Execution in the browser process.
Affected files:
content/browser/devtools/protocol/target_handler.cccontent/browser/devtools/protocol/target_handler.h
Estimated timestamp from git blame: 2025-05-26
Vulnerability Summary
A logic error in TargetHandler::AutoAttacherDestroyed within content/browser/devtools/protocol/target_handler.cc leads to iterator invalidation and a potential Use-After-Free (UAF) in the browser process. While the code correctly creates a local copy of the throttles_ container (a base::flat_set), it mistakenly iterates over the member variable itself instead of the copy. During iteration, calling throttle->Clear() results in the current throttle being erased from the throttles_ set, invalidating the loop’s iterators. Furthermore, synchronous callbacks during navigation resumption can cause the container’s underlying buffer to be reallocated, leading to a UAF.
Technical Details
In TargetHandler::AutoAttacherDestroyed (line 964), the code attempts to safely clear throttles associated with a destroyed TargetAutoAttacher:
void TargetHandler::AutoAttacherDestroyed(TargetAutoAttacher* auto_attacher) {
auto throttles = throttles_; // [1] Local copy created
for (Throttle* throttle : throttles_) { // [2] TYPO: Iterates the member 'throttles_' instead of the copy 'throttles'
if (throttle->auto_attacher() == auto_attacher) {
throttle->Clear(); // [3] This eventually mutates 'throttles_'
}
}
// ...
}
At [1], a copy of throttles_ is created. However, at [2], the loop iterates over the member throttles_ itself. When throttle->Clear() is called at [3], it executes the following path:
Throttle::Clear()(line 676) callsCleanupPointers().Throttle::CleanupPointers()(line 658) callstarget_handler_->throttles_.erase(this).
Because throttles_ is a base::flat_set (internally a sorted std::vector), erasing an element invalidates all iterators to that element and any subsequent elements. This results in the loop’s range-based iterator becoming invalid after the first erasure.
Re-entrancy and Use-After-Free
The impact is exacerbated because Throttle::Clear() also calls Resume() (line 682) if the throttle was deferring a navigation. NavigationThrottle::Resume() can trigger synchronous callbacks that start new navigations, which may in turn call TargetHandler::Throttle’s constructor. This constructor inserts the new throttle into target_handler_->throttles_ (line 379), which can cause the underlying vector buffer to be reallocated.
When reallocation occurs, the range-based for loop’s cached iterators and end() pointer in AutoAttacherDestroyed point to freed heap memory. Subsequent iterations will read from this freed buffer, resulting in a Use-After-Free. The intended safe pattern for iterating and clearing these throttles is demonstrated in TargetHandler::ClearThrottles (lines 854-859), where a copy is correctly utilized.
Impact and Exploitability
An attacker can exploit this issue to achieve a sandbox escape from a compromised renderer process or via a malicious website if a DevTools session is active (e.g., using Puppeteer or Selenium). By grooming the heap after the vector reallocation, an attacker can control the contents of the freed buffer. When the loop continues, it will read a malicious pointer from the freed buffer and call virtual methods (e.g., throttle->auto_attacher() or throttle->Clear()), leading to arbitrary code execution in the privileged browser process.
Notably, MiraclePtr (BRP) does not mitigate this vulnerability because the Use-After-Free occurs on the container’s internal buffer of pointers, rather than on the objects pointed to by raw_ptr themselves. This allows for direct control of the program’s execution flow if the heap can be sufficiently groomed.
Potential Reproduction Steps
These are suggested steps; our tooling agent doesn’t yet have the ability to run code to produce a working exploit:
- Start Chrome with
--remote-debugging-port. - Attach a CDP client and send
Target.setAutoAttach {autoAttach:true, waitForDebuggerOnStart:true, flatten:true}to a page. - Have the page create multiple cross-origin iframes (OOPIFs). This creates multiple
TargetHandler::ResponseThrottleinstances that are in a deferred state. - Navigate the main frame to a different origin (e.g.,
location.href = 'https://other-origin.com'). This destroys theRenderFrameDevToolsAgentHostand itsAutoAttacher. - Observe the crash in
TargetHandler::AutoAttacherDestroyedas it iterates overthrottles_while they are being erased.
Suggested Fix
Change the loop variable in TargetHandler::AutoAttacherDestroyed to iterate over the local copy instead of the member variable:
--- a/content/browser/devtools/protocol/target_handler.cc
+++ b/content/browser/devtools/protocol/target_handler.cc
@@ -963,7 +963,7 @@
void TargetHandler::AutoAttacherDestroyed(TargetAutoAttacher* auto_attacher) {
auto throttles = throttles_;
- for (Throttle* throttle : throttles_) {
+ for (Throttle* throttle : throttles) {
if (throttle->auto_attacher() == auto_attacher) {
throttle->Clear();
}
Evaluated with Chrome root at commit: 876d480da1f794d87813cfa2e6ff4fcf9771e939
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.