High watch CVSS 9.6 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DevTools
DescriptionUse after free in DevTools
ComponentDevTools
Sub-componentPlatform>DevTools
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.
Tracker553119925
Fix commit76d750b58133 (chromium/src) +77/-5
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-08-26 (per vendor advisory)
Fix landed2026-08-31 (public git)
Disclosed2026-09-03
Patch gap3 days — fix public in source before the advisory

Background

`TargetHandler`
the browser-side DevTools protocol handler that manages auto-attachment of debugging sessions to related targets (frames, workers, hidden targets).
`DevToolsAgentHost`
the ref-counted object representing a debuggable target; auto_attached_sessions_ maps a bare DevToolsAgentHost* key to its auto-attach Session.
`AutoDetach` / auto-attach cascade
detaching one target’s session can synchronously dispose that target’s child sessions and owned resources (e.g. hidden targets), which may destroy sibling DevToolsAgentHost instances and mutate auto_attached_sessions_.
Use-after-free
dereferencing a bare pointer key or Session object whose owning DevToolsAgentHost was freed by such a synchronous cascade.

Root Cause Analysis

TargetHandler::SetAttachedTargetsOfType needed to detach the currently auto-attached hosts that are absent from new_hosts, and it iterated a copy, auto_sessions = auto_attached_sessions_, calling AutoDetach(source, host.get()) inline for each match. Detaching a target (such as a subframe) can synchronously dispose its child sessions and tear down owned hidden targets, destroying sibling DevToolsAgentHost instances; those siblings remain as bare-pointer keys (and Session values referenced via entry.second->auto_attacher_id_) inside the old_sessions copy, so the loop’s later iterations dereference freed memory. The copy taking scoped_refptr only for the current entry.first did not protect the other, not-yet-visited entries whose hosts the cascade destroyed.

The fix separates enumeration from mutation: it first walks the live auto_attached_sessions_ while all entries are still valid and collects scoped_refptr<DevToolsAgentHost> into hosts_to_detach, then performs all AutoDetach calls afterward; because AutoDetach re-validates each host against the live map, any host already torn down by an earlier cascade is safely skipped, and the subsequent auto-attach loop consults the live auto_attached_sessions_ rather than the stale copy.

Key insight
The core mistake was mutating a container (auto_attached_sessions_) mid-iteration through calls (AutoDetach) that synchronously destroy other entries’ objects, leaving the loop holding dangling bare-pointer keys and Session references. The fix collects strong references to every host to detach before any detachment runs, so no entry is freed while still needed by the iteration.

Attack Path

  1. Auto-attach related targets A DevTools client (or page-driven automation) calls Target.autoAttachRelated for a page, then creates an OOPIF that is auto-attached as a child session.
  2. Create many sibling hidden targets Under the iframe session, the client creates numerous hidden targets (the test uses 20) and auto-attaches each, populating auto_attached_sessions_ with many sibling DevToolsAgentHost entries.
  3. Trigger a cascading detach A cross-site navigation detaches the iframe and synchronously disposes its hidden targets, so while SetAttachedTargetsOfType iterates, one AutoDetach destroys sibling DevToolsAgentHost instances and mutates the map.
  4. Dereference freed sibling (pre-patch) A later loop iteration constructs scoped_refptr<DevToolsAgentHost> host(entry.first) from, or reads entry.second->auto_attacher_id_ of, a now-freed sibling entry in the stale copy — a use-after-free in the browser process.
  5. Crash / corruption The dangling access produces a browser-process UAF (the regression test asserts a clean detach without crash); escalation beyond the crash is (inferred).

Impact Assessment

A DevTools client able to auto-attach related targets and induce a cascading detach (e.g. via an OOPIF with hidden targets and a cross-site navigation) can trigger a use-after-free of DevToolsAgentHost/Session objects in the browser process. The advisory classifies this as “Use after free in DevTools” and does not report in-the-wild exploitation; escalation from the UAF to a controlled read/write or code execution is (inferred) and not shown by the patch. Preconditions include DevTools protocol access with auto-attach enabled (locally, or via an automation/extension surface that reaches these commands).

Changed Functions

FunctionChangeNotes
for
content/browser/devtools/protocol/target_handler.cc
modified
for
third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach.js
modified
if
third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach.js
modified

Files Changed

  • third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach.js
  • third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach-expected.txt
  • content/browser/devtools/protocol/target_handler.cc

Audit Directions

  • Mutation during iteration via callbacks
    Look for loops over containers of raw pointers where the loop body invokes teardown/detach/dispose calls that can synchronously erase or free other elements of the same container.
  • Copies that don't own their elements
    Flag auto x = member_container; copies of maps/sets whose keys or values are bare pointers, which give a false sense of iteration safety while the pointed-to objects can still be freed.
  • Enumerate-then-act refactors
    Prefer patterns that snapshot strong references to all targets before performing side-effecting operations, and re-validate each target against live state at the point of action, as AutoDetach does here.
From 76d750b581336a8426fc135270395b01b3af8e97 Mon Sep 17 00:00:00 2001
From: Maksim Sadym <[email protected]>
Date: Mon, 31 Aug 2026 13:09:41 -0700
Subject: [PATCH] Prevent UAF in TargetHandler::SetAttachedTargetsOfType

Collect strong DevToolsAgentHost references prior to detachment to prevent
use-after-free when in-loop detachment synchronously destroys sibling
agent hosts.

Bug: 553119925
Change-Id: I406b611dedd30fea7be7ffee1252e35493107480
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8301599
Reviewed-by: Andrey Kosyakov <[email protected]>
Commit-Queue: Maksim Sadym <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1689188}
---
diff --git a/content/browser/devtools/protocol/target_handler.cc b/content/browser/devtools/protocol/target_handler.cc
index 053e06a..7e3c947d 100644
--- a/content/browser/devtools/protocol/target_handler.cc
+++ b/content/browser/devtools/protocol/target_handler.cc
@@ -970,18 +970,30 @@
     const base::flat_set<scoped_refptr<DevToolsAgentHost>>& new_hosts,
     const std::string& type) {
   DCHECK(!type.empty());
-  auto old_sessions = auto_attached_sessions_;
-  for (auto& entry : old_sessions) {
+  // Detaching a target (e.g. a subframe) can synchronously dispose child
+  // sessions and tear down owned resources (e.g. hidden targets), which may
+  // destroy sibling DevToolsAgentHost instances and mutate
+  // `auto_attached_sessions_`.
+  // To avoid use-after-free on bare pointer keys or session objects, first
+  // collect strong references to the hosts that need to be detached while all
+  // entries are still alive and valid.
+  std::vector<scoped_refptr<DevToolsAgentHost>> hosts_to_detach;
+  for (const auto& entry : auto_attached_sessions_) {
     scoped_refptr<DevToolsAgentHost> host(entry.first);
     if (host->GetType() == type &&
         entry.second->auto_attacher_id_ ==
             reinterpret_cast<uintptr_t>(source) &&
         !new_hosts.contains(host)) {
-      AutoDetach(source, host.get());
+      hosts_to_detach.push_back(std::move(host));
     }
   }
-  for (auto& host : new_hosts) {
-    if (!old_sessions.contains(host.get())) {
+  // AutoDetach re-validates each host against the live `auto_attached_sessions_`
+  // map, safely skipping any targets already detached by earlier cascades.
+  for (const auto& host : hosts_to_detach) {
+    AutoDetach(source, host.get());
+  }
+  for (const auto& host : new_hosts) {
+    if (!auto_attached_sessions_.contains(host.get())) {
       AutoAttach(source, host.get(), false);
     }
   }
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach-expected.txt
new file mode 100644
index 0000000..35bd945
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach-expected.txt
@@ -0,0 +1,4 @@
+Tests that navigating a page with an auto-attached iframe and hidden targets detaches cleanly without crashing.
+iframe session detached
+SUCCESS: Navigated and detached without crash
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach.js b/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach.js
new file mode 100644
index 0000000..fd3cfec
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach.js
@@ -0,0 +1,56 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  testRunner.log('Tests that navigating a page with an auto-attached iframe and hidden targets detaches cleanly without crashing.');
+  const bp = testRunner.browserP();
+
+  const page = await testRunner.createPage();
+  const pageSession = await page.createSession();
+
+  // Auto-attach related targets for the page.
+  await bp.Target.autoAttachRelated({
+    targetId: page.targetId(),
+    waitForDebuggerOnStart: false,
+  });
+
+  // Create an OOPIF.
+  const [attached] = await Promise.all([
+    bp.Target.onceAttachedToTarget(),
+    pageSession.evaluate(() => {
+      const iframe = document.createElement('iframe');
+      iframe.src = 'http://devtools.oopif.test:8080/inspector-protocol/resources/iframe.html';
+      document.body.appendChild(iframe);
+    }),
+  ]);
+  const iframeSession = testRunner.createSessionFor(attached.params.sessionId);
+
+  // Create hidden targets under the iframe session and auto-attach them.
+  const kHiddenTargetCount = 20;
+  for (let i = 0; i < kHiddenTargetCount; ++i) {
+    const {result: hiddenTarget} =
+        await iframeSession.protocol.Target.createTarget({
+          url: 'about:blank',
+          hidden: true,
+        });
+    await bp.Target.autoAttachRelated({
+      targetId: hiddenTarget.targetId,
+      waitForDebuggerOnStart: false,
+    });
+  }
+
+  // Cross-site navigation detaches the iframe and disposes its hidden targets.
+  await page.navigate('http://devtools.oopif-b.test:8080/inspector-protocol/resources/iframe.html');
+
+  // Assert that iframeSession was cleanly detached.
+  const evalResult = await iframeSession.protocol.Runtime.evaluate({expression: '1 + 1'});
+  if (evalResult.error) {
+    testRunner.log('iframe session detached');
+  } else {
+    testRunner.log('ERROR: iframe session should be detached but it was not');
+  }
+
+  testRunner.log('SUCCESS: Navigated and detached without crash');
+  testRunner.completeTest();
+});
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach-expected.txt
new file mode 100644
index 0000000..35bd945
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach-expected.txt
@@ -0,0 +1,4 @@
+Tests that navigating a page with an auto-attached iframe and hidden targets detaches cleanly without crashing.
+iframe session detached
+SUCCESS: Navigated and detached without crash
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach.js b/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach.js
new file mode 100644
index 0000000..fd3cfec
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach.js
@@ -0,0 +1,56 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  testRunner.log('Tests that navigating a page with an auto-attached iframe and hidden targets detaches cleanly without crashing.');
+  const bp = testRunner.browserP();
+
+  const page = await testRunner.createPage();
+  const pageSession = await page.createSession();
+
+  // Auto-attach related targets for the page.
+  await bp.Target.autoAttachRelated({
+    targetId: page.targetId(),
+    waitForDebuggerOnStart: false,
+  });
+
+  // Create an OOPIF.
+  const [attached] = await Promise.all([
+    bp.Target.onceAttachedToTarget(),
+    pageSession.evaluate(() => {
+      const iframe = document.createElement('iframe');
+      iframe.src = 'http://devtools.oopif.test:8080/inspector-protocol/resources/iframe.html';
+      document.body.appendChild(iframe);
+    }),
+  ]);
+  const iframeSession = testRunner.createSessionFor(attached.params.sessionId);
+
+  // Create hidden targets under the iframe session and auto-attach them.
+  const kHiddenTargetCount = 20;
+  for (let i = 0; i < kHiddenTargetCount; ++i) {
+    const {result: hiddenTarget} =
+        await iframeSession.protocol.Target.createTarget({
+          url: 'about:blank',
+          hidden: true,
+        });
+    await bp.Target.autoAttachRelated({
+      targetId: hiddenTarget.targetId,
+      waitForDebuggerOnStart: false,
+    });
+  }
+
+  // Cross-site navigation detaches the iframe and disposes its hidden targets.
+  await page.navigate('http://devtools.oopif-b.test:8080/inspector-protocol/resources/iframe.html');
+
+  // Assert that iframeSession was cleanly detached.
+  const evalResult = await iframeSession.protocol.Runtime.evaluate({expression: '1 + 1'});
+  if (evalResult.error) {
+    testRunner.log('iframe session detached');
+  } else {
+    testRunner.log('ERROR: iframe session should be detached but it was not');
+  }
+
+  testRunner.log('SUCCESS: Navigated and detached without crash');
+  testRunner.completeTest();
+});
Loading diff…

Original Bug Report

reported by [email protected]

Browser-Process UAF in TargetHandler::SetAttachedTargetsOfType via Bare Pointer Map Key

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: TargetHandler::SetAttachedTargetsOfType iterates over a stale copy of auto_attached_sessions_ containing bare DevToolsAgentHost* keys. Calling AutoDetach during the loop triggers a synchronous teardown cascade that can destroy sibling DevToolsAgentHost instances. Subsequent loop iterations dereference the freed bare pointer keys to perform AddRef() and virtual method calls, leading to potential browser-process use-after-free.

Affected files:

  • content/browser/devtools/protocol/target_handler.cc
  • content/browser/devtools/protocol/target_handler.h

Estimated timestamp from git blame: 2021-06-25

Summary

A potential Use-After-Free (UAF) vulnerability exists in TargetHandler::SetAttachedTargetsOfType in the Chromium browser process UI thread. The method creates a copy of auto_attached_sessions_ and iterates over it using bare DevToolsAgentHost* pointers as keys. When AutoDetach is invoked inside the loop, the synchronous teardown of child sessions can destroy sibling DevToolsAgentHost instances. Subsequent loop iterations dereference these dangling keys to perform refcount modifications and virtual method calls.

Location

  • content/browser/devtools/protocol/target_handler.cc:968-988
  • content/browser/devtools/protocol/target_handler.h:216-217

Root Cause Analysis

In content/browser/devtools/protocol/target_handler.h:216-217, auto_attached_sessions_ is declared as:

std::map<DevToolsAgentHost*, raw_ptr<Session, CtnExperimental>>
    auto_attached_sessions_;

Notice that the map key is a bare DevToolsAgentHost* pointer and is not protected by MiraclePtr (raw_ptr).

In content/browser/devtools/protocol/target_handler.cc:968-988:

void TargetHandler::SetAttachedTargetsOfType(
    TargetAutoAttacher* source,
    const base::flat_set<scoped_refptr<DevToolsAgentHost>>& new_hosts,
    const std::string& type) {
  DCHECK(!type.empty());
  auto old_sessions = auto_attached_sessions_;            // 973: Stale copy
  for (auto& entry : old_sessions) {
    scoped_refptr<DevToolsAgentHost> host(entry.first);   // 975: AddRef() through bare key
    if (host->GetType() == type &&                        // 976: Virtual call on bare key
        entry.second->auto_attacher_id_ ==
            reinterpret_cast<uintptr_t>(source) &&
        !new_hosts.contains(host)) {
      AutoDetach(source, host.get());                     // 980: Synchronous teardown cascade
    }
  }
  for (auto& host : new_hosts) {
    if (!old_sessions.contains(host.get())) {
      AutoAttach(source, host.get(), false);
    }
  }
}

When AutoDetach(source, host.get()) is called at line 980 on an attached iframe session S_F:

  1. TargetHandler::AutoDetach calls Session::Detach(false) (target_handler.cc:965).
  2. Session::Detach calls agent_host_->DetachClient(this) (target_handler.cc:534) -> DevToolsAgentHostImpl::DetachInternal (devtools_agent_host_impl.cc:349) -> session->Dispose() (devtools_agent_host_impl.cc:366).
  3. DevToolsSession::Dispose invokes Disable() on all handlers of child session S_F (devtools_session.cc:179-180).
  4. S_F’s TargetHandler::Disable() calls hidden_target_manager_.Clear() (target_handler.cc:787), destroying all unique_ptr<WebContents> held for hidden targets created under that child session (hidden_target_manager.cc:50).
  5. ~WebContentsImpl() tears down the frame tree and dispatches WebContentsObserver::FrameDeleted (web_contents_impl.cc:11730-11745).
  6. RenderFrameDevToolsAgentHost::FrameDeleted invokes DestroyOnRenderFrameGone() (render_frame_devtools_agent_host.cc:666), which detaches active sessions via ForceDetachAllSessionsImpl() (:687), causing the root session T_c to drop its strong scoped_refptr on the hidden target host (target_handler.cc:538).
  7. DestroyOnRenderFrameGone() calls Release() (render_frame_devtools_agent_host.cc:694), balancing the constructor’s AddRef(). Since no other references exist, the RenderFrameDevToolsAgentHost is deleted and freed.
  8. Execution returns to the loop in SetAttachedTargetsOfType. Because old_sessions holds the deallocated pointer at entry.first, line 975 executes scoped_refptr<DevToolsAgentHost> host(entry.first) which invokes AddRef() (writing ++ref_count_ inside freed memory), followed by line 976 calling host->GetType() (an indirect virtual call dereferencing the deallocated vtable pointer).

Suggested / Potential Reproduction Steps

(Note: These steps are derived from static source code analysis and modeling; automated execution of exploit code was not performed.)

  1. Launch Chrome with remote debugging enabled and an automation client that exposes the DevTools protocol to a target page via Target.exposeDevToolsProtocol (creating a privileged session T_c connected to window.cdp).
  2. From the target page, embed a cross-site iframe F.
  3. Send Target.autoAttachRelated({targetId: <PageTargetId>, filter: [{}]}) over window.cdp to auto-attach iframe F under T_c, yielding child session S_F.
  4. Send Target.createTarget({url: <url>, hidden: true}) commands through session S_F to spawn hidden targets H_1, …, H_n.
  5. Send Target.autoAttachRelated({targetId: <H_k>, filter: [{}]}) for each hidden target to attach them under root session T_c.
  6. Navigate the top frame cross-site (location.href = ...). The navigation updates the frame subtree, invoking FrameAutoAttacher::UpdateFrames() which calls SetAttachedTargetsOfType without iframe F.
  7. SetAttachedTargetsOfType detaches F, disposing S_F and destroying the hidden WebContents and their RenderFrameDevToolsAgentHost instances inline. As the loop iterates over old_sessions, it dereferences the freed DevToolsAgentHost* key.

Suggested Fix

Avoid iterating over stale bare pointers while executing reentrant or synchronous detachment operations. Similar patterns in the same file address this by collecting strong references before detachment:

std::vector<scoped_refptr<DevToolsAgentHost>> hosts_to_detach;
for (const auto& entry : auto_attached_sessions_) {
  if (entry.first->GetType() == type &&
      entry.second->auto_attacher_id_ == reinterpret_cast<uintptr_t>(source) &&
      !new_hosts.contains(entry.first)) {
    hosts_to_detach.push_back(entry.first);
  }
}
for (const auto& host : hosts_to_detach) {
  AutoDetach(source, host.get());
}

Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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