CVE-2026-87533
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Fcontent/browser/devtools/protocol/devtools_protocol_browsertest.cc |
modified | |
forcontent/browser/devtools/protocol/devtools_protocol_browsertest.cc |
modified | |
ifcontent/browser/devtools/protocol/devtools_protocol_browsertest.cc |
modified |
Files Changed
content/browser/devtools/protocol/devtools_protocol_browsertest.cccontent/browser/devtools/protocol/target_handler.cc
Patch
From d5186c8dddab74ef4874b27aba84f942c16efd16 Mon Sep 17 00:00:00 2001 From: Alex Rudenko <[email protected]> Date: Mon, 03 Aug 2026 03:00:48 -0700 Subject: [PATCH] DevTools: avoid reentrant erase during TargetHandler::Disable TargetHandler::Disable() bulk-clears `attached_sessions_`, but destroying a Session detaches its child DevTools session from the target's agent host. The embedder side of that child session may own hidden targets which get closed at this point; if their renderers are gone the close path runs synchronously and the hidden targets' agent hosts call AgentHostClosed() on sibling Sessions still owned by the map being cleared, which then call `attached_sessions_.erase(id_)` on it. Move the map into a local before releasing it so the reentrant erase operates on the now-empty member instead of the container being walked. Fixed: 521620916 Change-Id: Iaa54071504b73d54ac1f18cfc67514f42e1c3d31 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8182568 Commit-Queue: Alex Rudenko <[email protected]> Reviewed-by: Andrey Kosyakov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1672509} --- diff --git a/content/browser/devtools/protocol/devtools_protocol_browsertest.cc b/content/browser/devtools/protocol/devtools_protocol_browsertest.cc index 1174671f..382d5c3c 100644 --- a/content/browser/devtools/protocol/devtools_protocol_browsertest.cc +++ b/content/browser/devtools/protocol/devtools_protocol_browsertest.cc @@ -5374,4 +5374,108 @@ EXPECT_EQ("Internal error", *error_message); } +// Regression test for crbug.com/521620916: detaching the browser client when +// hidden targets with crashed renderers are attached should not cause +// reentrancy UAF when sessions are cleared. +IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, DetachWithCrashedHiddenTargets) { + content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes; + set_agent_host_can_close(); + AttachToBrowserTarget(); + + base::DictValue create_page_params; + create_page_params.Set("url", "about:blank"); + const base::DictValue* result = + SendCommandSync("Target.createTarget", std::move(create_page_params)); + ASSERT_TRUE(result); + const std::string* page_target_id_ptr = result->FindString("targetId"); + ASSERT_TRUE(page_target_id_ptr); + std::string page_target_id = *page_target_id_ptr; + + scoped_refptr<DevToolsAgentHost> page_agent_host = + DevToolsAgentHost::GetForId(page_target_id); + ASSERT_TRUE(page_agent_host); + EXPECT_TRUE(WaitForLoadStop(page_agent_host->GetWebContents())); + + base::DictValue attach_params; + attach_params.Set("targetId", page_target_id); + attach_params.Set("flatten", true); + result = SendCommandSync("Target.attachToTarget", std::move(attach_params)); + ASSERT_TRUE(result); + const std::string* page_session_id_ptr = result->FindString("sessionId"); + ASSERT_TRUE(page_session_id_ptr); + std::string page_session_id = *page_session_id_ptr; + + // Use 12 targets to ensure sufficient depth and branching in libc++'s + // std::map Red-Black tree so that reentrant erase() calls during clearance + // reliably collide with post-order traversal regardless of key ordering. + constexpr size_t kHiddenTargetCount = 12; + std::vector<std::string> hidden_target_ids; + for (size_t i = 0; i < kHiddenTargetCount; ++i) { + base::DictValue params; + params.Set("url", "about:blank"); + params.Set("hidden", true); + const base::DictValue* create_result = SendSessionCommand( + "Target.createTarget", std::move(params), page_session_id, true); + ASSERT_TRUE(create_result); + const std::string* target_id = create_result->FindString("targetId"); + ASSERT_TRUE(target_id); + hidden_target_ids.push_back(*target_id); + } + + for (const std::string& target_id : hidden_target_ids) { + scoped_refptr<DevToolsAgentHost> agent_host = + DevToolsAgentHost::GetForId(target_id); + ASSERT_TRUE(agent_host); + WebContents* web_contents = agent_host->GetWebContents(); + ASSERT_TRUE(web_contents); + EXPECT_TRUE(WaitForLoadStop(web_contents)); + } + + for (const std::string& target_id : hidden_target_ids) { + base::DictValue params; + params.Set("targetId", target_id); + params.Set("flatten", true); + SendCommandSync("Target.attachToTarget", std::move(params)); + } + + // Terminate each hidden target's renderer so that closing the page on + // detach takes the synchronous path. The browser session is then detached + // during fixture tear down, which closes the hidden targets while the + // owning child session is being released. + std::set<RenderProcessHost*> rphs; + for (const std::string& target_id : hidden_target_ids) { + scoped_refptr<DevToolsAgentHost> agent_host = + DevToolsAgentHost::GetForId(target_id); + ASSERT_TRUE(agent_host); + WebContents* web_contents = agent_host->GetWebContents(); + ASSERT_TRUE(web_contents); + RenderProcessHost* rph = web_contents->GetPrimaryMainFrame()->GetProcess(); + if (rph) { + rphs.insert(rph); + } + } + + for (RenderProcessHost* rph : rphs) { + if (rph && rph->IsInitializedAndNotDead()) { + RenderProcessHostWatcher watcher( + rph, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); + rph->Shutdown(RESULT_CODE_KILLED); + watcher.Wait(); + } + } + + for (const std::string& target_id : hidden_target_ids) { + scoped_refptr<DevToolsAgentHost> agent_host = + DevToolsAgentHost::GetForId(target_id); + ASSERT_TRUE(agent_host); + WebContents* web_contents = agent_host->GetWebContents(); + ASSERT_TRUE(web_contents); + ASSERT_FALSE(web_contents->GetPrimaryMainFrame()->IsRenderFrameLive()); + } + + // Detaching the browser client closes the page session and hidden targets + // synchronously; should not crash or trigger reentrancy UAF. + Detach(); +} + } // namespace content diff --git a/content/browser/devtools/protocol/target_handler.cc b/content/browser/devtools/protocol/target_handler.cc index f051aaf1..f20a1a8 100644 --- a/content/browser/devtools/protocol/target_handler.cc +++ b/content/browser/devtools/protocol/target_handler.cc @@ -792,7 +792,10 @@ SetDiscoverTargets(false, {}); hidden_target_manager_.Clear(); auto_attached_sessions_.clear(); - attached_sessions_.clear(); + // Destroying a session may synchronously close attached targets which can + // try to detach sibling sessions, so move the map aside before releasing. + auto attached_sessions = std::move(attached_sessions_); + attached_sessions.clear(); DevToolsManagerDelegate* delegate = DevToolsManager::GetInstance()->delegate();
Regression Test / PoC
diff --git a/content/browser/devtools/protocol/devtools_protocol_browsertest.cc b/content/browser/devtools/protocol/devtools_protocol_browsertest.cc
index 1174671f..382d5c3c 100644
--- a/content/browser/devtools/protocol/devtools_protocol_browsertest.cc
+++ b/content/browser/devtools/protocol/devtools_protocol_browsertest.cc
@@ -5374,4 +5374,108 @@
EXPECT_EQ("Internal error", *error_message);
}
+// Regression test for crbug.com/521620916: detaching the browser client when
+// hidden targets with crashed renderers are attached should not cause
+// reentrancy UAF when sessions are cleared.
+IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, DetachWithCrashedHiddenTargets) {
+ content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes;
+ set_agent_host_can_close();
+ AttachToBrowserTarget();
+
+ base::DictValue create_page_params;
+ create_page_params.Set("url", "about:blank");
+ const base::DictValue* result =
+ SendCommandSync("Target.createTarget", std::move(create_page_params));
+ ASSERT_TRUE(result);
+ const std::string* page_target_id_ptr = result->FindString("targetId");
+ ASSERT_TRUE(page_target_id_ptr);
+ std::string page_target_id = *page_target_id_ptr;
+
+ scoped_refptr<DevToolsAgentHost> page_agent_host =
+ DevToolsAgentHost::GetForId(page_target_id);
+ ASSERT_TRUE(page_agent_host);
+ EXPECT_TRUE(WaitForLoadStop(page_agent_host->GetWebContents()));
+
+ base::DictValue attach_params;
+ attach_params.Set("targetId", page_target_id);
+ attach_params.Set("flatten", true);
+ result = SendCommandSync("Target.attachToTarget", std::move(attach_params));
+ ASSERT_TRUE(result);
+ const std::string* page_session_id_ptr = result->FindString("sessionId");
+ ASSERT_TRUE(page_session_id_ptr);
+ std::string page_session_id = *page_session_id_ptr;
+
+ // Use 12 targets to ensure sufficient depth and branching in libc++'s
+ // std::map Red-Black tree so that reentrant erase() calls during clearance
+ // reliably collide with post-order traversal regardless of key ordering.
+ constexpr size_t kHiddenTargetCount = 12;
+ std::vector<std::string> hidden_target_ids;
+ for (size_t i = 0; i < kHiddenTargetCount; ++i) {
+ base::DictValue params;
+ params.Set("url", "about:blank");
+ params.Set("hidden", true);
+ const base::DictValue* create_result = SendSessionCommand(
+ "Target.createTarget", std::move(params), page_session_id, true);
+ ASSERT_TRUE(create_result);
+ const std::string* target_id = create_result->FindString("targetId");
+ ASSERT_TRUE(target_id);
+ hidden_target_ids.push_back(*target_id);
+ }
+
+ for (const std::string& target_id : hidden_target_ids) {
+ scoped_refptr<DevToolsAgentHost> agent_host =
+ DevToolsAgentHost::GetForId(target_id);
+ ASSERT_TRUE(agent_host);
+ WebContents* web_contents = agent_host->GetWebContents();
+ ASSERT_TRUE(web_contents);
+ EXPECT_TRUE(WaitForLoadStop(web_contents));
+ }
+
+ for (const std::string& target_id : hidden_target_ids) {
+ base::DictValue params;
+ params.Set("targetId", target_id);
+ params.Set("flatten", true);
+ SendCommandSync("Target.attachToTarget", std::move(params));
+ }
+
+ // Terminate each hidden target's renderer so that closing the page on
+ // detach takes the synchronous path. The browser session is then detached
+ // during fixture tear down, which closes the hidden targets while the
+ // owning child session is being released.
+ std::set<RenderProcessHost*> rphs;
+ for (const std::string& target_id : hidden_target_ids) {
+ scoped_refptr<DevToolsAgentHost> agent_host =
+ DevToolsAgentHost::GetForId(target_id);
+ ASSERT_TRUE(agent_host);
+ WebContents* web_contents = agent_host->GetWebContents();
+ ASSERT_TRUE(web_contents);
+ RenderProcessHost* rph = web_contents->GetPrimaryMainFrame()->GetProcess();
+ if (rph) {
+ rphs.insert(rph);
+ }
+ }
+
+ for (RenderProcessHost* rph : rphs) {
+ if (rph && rph->IsInitializedAndNotDead()) {
+ RenderProcessHostWatcher watcher(
+ rph, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
+ rph->Shutdown(RESULT_CODE_KILLED);
+ watcher.Wait();
+ }
+ }
+
+ for (const std::string& target_id : hidden_target_ids) {
+ scoped_refptr<DevToolsAgentHost> agent_host =
+ DevToolsAgentHost::GetForId(target_id);
+ ASSERT_TRUE(agent_host);
+ WebContents* web_contents = agent_host->GetWebContents();
+ ASSERT_TRUE(web_contents);
+ ASSERT_FALSE(web_contents->GetPrimaryMainFrame()->IsRenderFrameLive());
+ }
+
+ // Detaching the browser client closes the page session and hidden targets
+ // synchronously; should not crash or trigger reentrancy UAF.
+ Detach();
+}
+
} // namespace content
Original Bug Report
Browser-process Use-After-Free/Double-Free in content::TargetHandler::Disable
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: A potential high-severity heap use-after-free and double-free vulnerability exists in the browser process due to reentrancy in content::TargetHandler::Disable(). Synchronous destruction of target sessions during map clearance can trigger a recursive deletion cascade that modifies the same map mid-operation. This corrupts the underlying libc++ tree structure, leading to memory corruption without MiraclePtr protection.
Affected files:
content/browser/devtools/protocol/target_handler.cccontent/browser/devtools/protocol/target_handler.h
Estimated timestamp from git blame: 2017-07-20
Summary
There is a potential reentrancy-driven heap use-after-free (UAF) and double-free vulnerability in the browser process within content::TargetHandler::Disable(). When clearing attached_sessions_ (a std::map<std::string, std::unique_ptr<Session>>), the value destructor (~Session()) can run synchronously. Under specific teardown conditions (such as when a renderer process has crashed), this destruction triggers a recursive cascade that synchronously unlinks and deallocates sibling entries from the same map via S_Bi->AgentHostClosed() -> Detach(true).
Because libc++ __tree::clear() is not reentrancy-safe (it performs a recursive post-order traversal and caches the right-hand child node pointer before invoking the value destructor), the reentrant erase() call executes a search starting from the tree root. This results in Use-After-Free reads on already-freed tree node pointers and keys. Furthermore, if a sibling entry resides in the cached right subtree, the inner erase() deallocates it immediately, causing a Use-After-Free dereference, double-destruction of its unique_ptr payload, and a double-free of the node structure when the outer clear() loop subsequently attempts to visit it.
Affected Locations
content/browser/devtools/protocol/target_handler.cc(specifically withinTargetHandler::Disable(),~Session(), andSession::Detach()).content/browser/devtools/protocol/target_handler.h
Potential Trigger Mechanism
Note: The following are potential steps to trigger the issue, as our automated tooling does not currently have the capability to execute code or run a working Proof-of-Concept.
- Establish a trusted Chrome DevTools Protocol (CDP) client session
S1connected to the root browser session (e.g., via--remote-debugging-pipe). - Attach a child session
S_XviaTarget.attachToTargetwith theflatten: trueoption. This populatesT1->attached_sessions_and instantiates a headless sessionHDS_Xand headless handlerHT_X. - Through child session
sX, create several hidden targets $B_1 \dots B_n$ viaTarget.createTargetwithhidden: true. This places the hidden target IDs intoHT_X->hidden_web_contents_. - Attach all hidden targets $B_1 \dots B_n$ to the root session
S1usingTarget.attachToTarget(Bi). This stores sibling sessionsS_BialongsideS_XinT1->attached_sessions_. - Crash the renderer processes associated with the hidden targets $B_1 \dots B_n$, so that their respective main frames report
IsRenderFrameLive() == false. - Close the root pipe connection
S1. This triggersBrowserDevToolsAgentHost::DetachInternal(S1)on the UI thread, invokingT1->Disable()and callingattached_sessions_.clear(). - When the outer
__tree::clear()deletes nodeS_X, its destructor synchronously detaches the agent host. Because the renderers are dead, this triggers a synchronous fallback branch inRenderFrameHostImpl::ClosePagethat destroys the headless WebContents synchronously. - The WebContents destruction fires frame deletion notifications synchronously, reaching the other attached sessions $S_{Bi}$ on
T1viaAgentHostClosed() -> Detach(true). - This triggers
handler_->attached_sessions_.erase(id_SBi)reentrantly onT1, corrupting the__treeduring the outerclear()operation.
Suggested Fix
To eliminate this reentrancy vulnerability, attached_sessions_ should be swapped to a local map before clearance, or cleared in a reentrancy-safe loop. Swapping the map ensures that any reentrant deletion calls operate on an empty map rather than mutating a container currently undergoing destruction.
Response TargetHandler::Disable() {
SetAutoAttachInternal(false, false, false, base::DoNothing());
SetDiscoverTargets(false, {});
hidden_target_manager_.Clear();
auto_attached_sessions_.clear();
// Swap to local map to prevent reentrancy issues during clearance
std::map<std::string, std::unique_ptr<Session>> attached_sessions;
attached_sessions.swap(attached_sessions_);
attached_sessions.clear();
...
Evaluated with Chrome root at commit: 3947e01999a53d4e2382e39736cb79d79c7dffcf
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.