Low chrome UAF 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DevTools
DescriptionUse after free in DevTools
ComponentDevTools
Bug ClassUAF
Tracker506193577
Fix commit61355c93e7c7 (chromium/src) +52/-3
CISA KEVNot listed
CreditedSyn4pse
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
ForEachDOMListener
third_party/blink/renderer/core/inspector/inspector_dom_agent.cc
modified
for
third_party/blink/renderer/core/inspector/inspector_dom_agent.cc
modified

Files Changed

  • third_party/blink/renderer/core/inspector/inspector_dom_agent.cc
  • third_party/blink/renderer/core/inspector/inspector_dom_agent.h
  • third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding-expected.txt
  • third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding.js
From 61355c93e7c7fb3b617d5bf3951b96195ad28643 Mon Sep 17 00:00:00 2001
From: Dmitry Gozman <dgozman@chromium.org>
Date: Sun, 28 Jun 2026 14:15:30 -0700
Subject: [PATCH] [CDP]: fix re-entrancy while notifying dom listeners

Bug: 506193577
Change-Id: Ied4b76214facff3bbb73c64b4686e8b6e5b0ef90
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8005056
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Commit-Queue: Dmitry Gozman <dgozman@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1653810}
---

diff --git a/third_party/blink/renderer/core/inspector/inspector_dom_agent.cc b/third_party/blink/renderer/core/inspector/inspector_dom_agent.cc
index b8ad8e910..58b9a0b3 100644
--- a/third_party/blink/renderer/core/inspector/inspector_dom_agent.cc
+++ b/third_party/blink/renderer/core/inspector/inspector_dom_agent.cc
@@ -507,18 +507,33 @@
 }
 
 void InspectorDOMAgent::NotifyDidAddDocument(Document* document) {
-  for (DOMListener* listener : dom_listeners_)
+  ForEachDOMListener([document](const Member<DOMListener>& listener) {
     listener->DidAddDocument(document);
+  });
 }
 
 void InspectorDOMAgent::NotifyWillRemoveDOMNode(Node* node) {
-  for (DOMListener* listener : dom_listeners_)
+  ForEachDOMListener([node](const Member<DOMListener>& listener) {
     listener->WillRemoveDOMNode(node);
+  });
 }
 
 void InspectorDOMAgent::NotifyDidModifyDOMAttr(Element* element) {
-  for (DOMListener* listener : dom_listeners_)
+  ForEachDOMListener([element](const Member<DOMListener>& listener) {
     listener->DidModifyDOMAttr(element);
+  });
+}
+
+void InspectorDOMAgent::ForEachDOMListener(
+    base::FunctionRef<void(const Member<DOMListener>&)> callback) {
+  // Notifying listeners may pause in debugger and thus cause all kinds of
+  // side effects, including adding/removing listeners, so make a copy.
+  HeapHashSet<Member<DOMListener>> copy(dom_listeners_);
+  for (const Member<DOMListener>& listener : copy) {
+    if (dom_listeners_.Contains(listener)) {
+      callback(listener);
+    }
+  }
 }
 
 void InspectorDOMAgent::SetDocument(Document* doc) {
diff --git a/third_party/blink/renderer/core/inspector/inspector_dom_agent.h b/third_party/blink/renderer/core/inspector/inspector_dom_agent.h
index 6b993d3f..af543c9 100644
--- a/third_party/blink/renderer/core/inspector/inspector_dom_agent.h
+++ b/third_party/blink/renderer/core/inspector/inspector_dom_agent.h
@@ -369,6 +369,7 @@
   void NotifyDidAddDocument(Document*);
   void NotifyWillRemoveDOMNode(Node*);
   void NotifyDidModifyDOMAttr(Element*);
+  void ForEachDOMListener(base::FunctionRef<void(const Member<DOMListener>&)>);
 
   // Node-related methods.
   using NodeToIdMap = GCedHeapHashMap<Member<Node>, int>;
diff --git a/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding-expected.txt b/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding-expected.txt
new file mode 100644
index 0000000..bd984f2
--- /dev/null
+++ b/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding-expected.txt
@@ -0,0 +1,6 @@
+Tests that disabling domains while paused on node removal DOM breakpoint works.
+removing the node
+paused in debugger, disabling domains
+resuming
+success
+
diff --git a/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding.js b/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding.js
new file mode 100644
index 0000000..b9cc0ee
--- /dev/null
+++ b/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding.js
@@ -0,0 +1,27 @@
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  var {page, session, dp} = await testRunner.startHTML(
+    '<style>#v{color:red}</style><div id=v>target</div><script>window.removeIt=()=>v.remove()</script>',
+    'Tests that disabling domains while paused on node removal DOM breakpoint works.');
+
+  await dp.Runtime.enable();
+  await dp.Debugger.enable();
+  await dp.DOM.enable();
+  await dp.CSS.enable();
+
+  const { result: doc } = await dp.DOM.getDocument({ depth: -1, pierce: true });
+  const { result: node } = await dp.DOM.querySelector({ nodeId: doc.root.nodeId, selector: '#v' });
+  await dp.DOMDebugger.setDOMBreakpoint({ nodeId: node.nodeId, type: 'node-removed' });
+
+  testRunner.log('removing the node');
+  const evaluated = dp.Runtime.evaluate({ expression: 'window.removeIt()' });
+  await dp.Debugger.oncePaused();
+  testRunner.log('paused in debugger, disabling domains');
+  await dp.CSS.disable();
+  await dp.DOM.disable();
+  testRunner.log('resuming');
+  await dp.Debugger.resume();
+  await evaluated;
+  testRunner.log('success');
+
+  testRunner.completeTest();
+})
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding-expected.txt b/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding-expected.txt
new file mode 100644
index 0000000..bd984f2
--- /dev/null
+++ b/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding-expected.txt
@@ -0,0 +1,6 @@
+Tests that disabling domains while paused on node removal DOM breakpoint works.
+removing the node
+paused in debugger, disabling domains
+resuming
+success
+
diff --git a/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding.js b/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding.js
new file mode 100644
index 0000000..b9cc0ee
--- /dev/null
+++ b/third_party/blink/web_tests/inspector-protocol/dom/dom-disable-while-unbinding.js
@@ -0,0 +1,27 @@
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+  var {page, session, dp} = await testRunner.startHTML(
+    '<style>#v{color:red}</style><div id=v>target</div><script>window.removeIt=()=>v.remove()</script>',
+    'Tests that disabling domains while paused on node removal DOM breakpoint works.');
+
+  await dp.Runtime.enable();
+  await dp.Debugger.enable();
+  await dp.DOM.enable();
+  await dp.CSS.enable();
+
+  const { result: doc } = await dp.DOM.getDocument({ depth: -1, pierce: true });
+  const { result: node } = await dp.DOM.querySelector({ nodeId: doc.root.nodeId, selector: '#v' });
+  await dp.DOMDebugger.setDOMBreakpoint({ nodeId: node.nodeId, type: 'node-removed' });
+
+  testRunner.log('removing the node');
+  const evaluated = dp.Runtime.evaluate({ expression: 'window.removeIt()' });
+  await dp.Debugger.oncePaused();
+  testRunner.log('paused in debugger, disabling domains');
+  await dp.CSS.disable();
+  await dp.DOM.disable();
+  testRunner.log('resuming');
+  await dp.Debugger.resume();
+  await evaluated;
+  testRunner.log('success');
+
+  testRunner.completeTest();
+})
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.