Medium watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DevTools
DescriptionUse after free in DevTools
ComponentDevTools
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497735587
Fix commit7ccce10b43ab (chromium/src) +51/-1
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-03-31 (public git)
Disclosed2026-05-05
Patch gap35 days — fix public in source before the advisory

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.
Key insight
The core mistake was a typo that iterated the live member 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

  1. Enable auto-attach via CDP A DevTools/CDP client calls Target.setAutoAttach with waitForDebuggerOnStart, so new targets are deferred by throttles registered in throttles_.
  2. Create multiple deferred targets The client creates several targets (the PoC creates three), populating throttles_ with multiple pending throttles.
  3. Trigger `AutoAttacherDestroyed` Calling setAutoAttach again drives RemoveClient → AutoAttacherDestroyed, entering the loop over the member throttles_.
  4. Re-entrant mutation during iteration throttle->Clear() removes entries and resumption callbacks reallocate the buffer, invalidating the loop’s iterators over throttles_ and leading to a use-after-free (inferred: no concrete exploitation primitive is shown by the patch).

Impact Assessment

An attacker with the ability to drive the DevTools/CDP protocol (e.g. an extension or client with debugger access) can trigger a use-after-free in the browser process via repeated auto-attach manipulation. The advisory rates this medium severity (use after free in DevTools) and does not report in-the-wild exploitation; escalation beyond the UAF is (inferred). The precondition is CDP access sufficient to register and tear down auto-attach throttles.

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

Audit Directions

  • Copy-then-iterate-wrong-container
    Grep 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 iteration
    Audit loops calling methods like Clear()/Remove() that re-enter and mutate the container, especially where synchronous callbacks may reallocate the backing store.
  • CDP-reachable handlers
    Review browser-process DevTools handlers that manage per-client collections (throttles, sessions) for lifetime hazards when clients are added/removed re-entrantly.
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