Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DevTools
DescriptionUse after free in DevTools
ComponentDevTools
Bug ClassUAF
Tracker497735587
Fix commit7ccce10b43ab (chromium/src) +51/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
for
content/browser/devtools/protocol/target_handler.cc
modified

Files Changed

  • content/browser/devtools/protocol/target_handler.cc
  • third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF-expected.txt
  • third_party/blink/web_tests/http/tests/inspector-protocol/target/target-setAutoAttach-destroy-UAF.js
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();
+})
Loading diff…

Regression Test / PoC

shipped with the fix
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();
+})
Loading diff…

Original Bug Report

reported by rj...@google.com

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.cc
  • content/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:

  1. Throttle::Clear() (line 676) calls CleanupPointers().
  2. Throttle::CleanupPointers() (line 658) calls target_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:

  1. Start Chrome with --remote-debugging-port.
  2. Attach a CDP client and send Target.setAutoAttach {autoAttach:true, waitForDebuggerOnStart:true, flatten:true} to a page.
  3. Have the page create multiple cross-origin iframes (OOPIFs). This creates multiple TargetHandler::ResponseThrottle instances that are in a deferred state.
  4. Navigate the main frame to a different origin (e.g., location.href = 'https://other-origin.com'). This destroys the RenderFrameDevToolsAgentHost and its AutoAttacher.
  5. Observe the crash in TargetHandler::AutoAttacherDestroyed as it iterates over throttles_ 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.

View on issue tracker
Links in the report