CVE-2026-7975
Overview
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
Patch
From 7ccce10b43ab3c90a589253f0121fd6206da01e1 Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
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 <caseq@chromium.org>
Commit-Queue: Andrey Kosyakov <caseq@chromium.org>
Auto-Submit: Danil Somsikov <dsv@chromium.org>
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.