CVE-2026-85042
Overview
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 bareDevToolsAgentHost*key to its auto-attachSession. - `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
DevToolsAgentHostinstances and mutateauto_attached_sessions_. - Use-after-free
- dereferencing a bare pointer key or
Sessionobject whose owningDevToolsAgentHostwas 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.
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
- Auto-attach related targets
A DevTools client (or page-driven automation) calls
Target.autoAttachRelatedfor a page, then creates an OOPIF that is auto-attached as a child session. - 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 siblingDevToolsAgentHostentries. - Trigger a cascading detach
A cross-site navigation detaches the iframe and synchronously disposes its hidden targets, so while
SetAttachedTargetsOfTypeiterates, oneAutoDetachdestroys siblingDevToolsAgentHostinstances and mutates the map. - Dereference freed sibling (pre-patch)
A later loop iteration constructs
scoped_refptr<DevToolsAgentHost> host(entry.first)from, or readsentry.second->auto_attacher_id_of, a now-freed sibling entry in the stale copy — a use-after-free in the browser process. - 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
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
| Function | Change | Notes |
|---|---|---|
forcontent/browser/devtools/protocol/target_handler.cc |
modified | |
forthird_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach.js |
modified | |
ifthird_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.jsthird_party/blink/web_tests/http/tests/inspector-protocol/target/auto-attach-related-hidden-targets-detach-expected.txtcontent/browser/devtools/protocol/target_handler.cc
Audit Directions
- Mutation during iteration via callbacksLook 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 elementsFlag
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 refactorsPrefer 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
AutoDetachdoes here.
Patch
From 76d750b581336a8426fc135270395b01b3af8e97 Mon Sep 17 00:00:00 2001
From: Maksim Sadym <sadym@chromium.org>
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 <caseq@chromium.org>
Commit-Queue: Maksim Sadym <sadym@chromium.org>
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();
+});
Regression Test / PoC
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();
+});
Original Bug Report
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.cccontent/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-988content/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:
TargetHandler::AutoDetachcallsSession::Detach(false)(target_handler.cc:965).Session::Detachcallsagent_host_->DetachClient(this)(target_handler.cc:534) ->DevToolsAgentHostImpl::DetachInternal(devtools_agent_host_impl.cc:349) ->session->Dispose()(devtools_agent_host_impl.cc:366).DevToolsSession::DisposeinvokesDisable()on all handlers of child session S_F (devtools_session.cc:179-180).- S_F’s
TargetHandler::Disable()callshidden_target_manager_.Clear()(target_handler.cc:787), destroying allunique_ptr<WebContents>held for hidden targets created under that child session (hidden_target_manager.cc:50). ~WebContentsImpl()tears down the frame tree and dispatchesWebContentsObserver::FrameDeleted(web_contents_impl.cc:11730-11745).RenderFrameDevToolsAgentHost::FrameDeletedinvokesDestroyOnRenderFrameGone()(render_frame_devtools_agent_host.cc:666), which detaches active sessions viaForceDetachAllSessionsImpl()(:687), causing the root session T_c to drop its strongscoped_refptron the hidden target host (target_handler.cc:538).DestroyOnRenderFrameGone()callsRelease()(render_frame_devtools_agent_host.cc:694), balancing the constructor’sAddRef(). Since no other references exist, theRenderFrameDevToolsAgentHostis deleted and freed.- Execution returns to the loop in
SetAttachedTargetsOfType. Becauseold_sessionsholds the deallocated pointer atentry.first, line 975 executesscoped_refptr<DevToolsAgentHost> host(entry.first)which invokesAddRef()(writing++ref_count_inside freed memory), followed by line 976 callinghost->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.)
- 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 towindow.cdp). - From the target page, embed a cross-site iframe F.
- Send
Target.autoAttachRelated({targetId: <PageTargetId>, filter: [{}]})overwindow.cdpto auto-attach iframe F under T_c, yielding child session S_F. - Send
Target.createTarget({url: <url>, hidden: true})commands through session S_F to spawn hidden targets H_1, …, H_n. - Send
Target.autoAttachRelated({targetId: <H_k>, filter: [{}]})for each hidden target to attach them under root session T_c. - Navigate the top frame cross-site (
location.href = ...). The navigation updates the frame subtree, invokingFrameAutoAttacher::UpdateFrames()which callsSetAttachedTargetsOfTypewithout iframe F. SetAttachedTargetsOfTypedetaches F, disposing S_F and destroying the hiddenWebContentsand theirRenderFrameDevToolsAgentHostinstances inline. As the loop iterates overold_sessions, it dereferences the freedDevToolsAgentHost*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.