CVE-2026-95373
Overview
Background
- `InspectorOverlayAgent`
- The Blink DevTools agent that manages page overlays (highlights, the “Paused in debugger” banner) and owns the currently active
InspectTool. - `PausedInDebuggerTool`
- An
InspectToolsubclass that renders the debugger-pause overlay and calls back into the V8 inspector session (e.g. resume/stepOver) viaExecuteOnV8Session. - `V8InspectorSession`
- The V8-side inspector session object, owned by the
DevToolsSession, which is destroyed when the session detaches; a raw pointer to it dangles after detach. - `WeakCellFactory`
- A Blink helper that hands out weak references so a pending task can still target a garbage-collected tool object after the agent has moved on to a different tool.
Root Cause Analysis
PausedInDebuggerTool stored the V8 inspector session as a bare raw_ptr<v8_inspector::V8InspectorSession, UnprotectedInRelease | DanglingUntriaged> and cleared it only from OnAgentDisable(), which was invoked when the overlay agent was disabled but not when the tool was merely swapped out via ClearInspectTool(). Because the tool binds ExecuteOnV8Session through its WeakCellFactory, a resume/stepOver request could leave a pending task that still reaches the orphaned tool; when DevToolsSession::Detach() then destroyed the V8InspectorSession, that task would make a virtual call through the now-dangling v8_session_ pointer, a use-after-free. The invariant violated is that the tool’s unowned reference to the session must be dropped whenever the tool stops being the active tool, not only on full agent disable.
The fix replaces the raw pointer with an owning/holding V8SessionHolder, renames the callback to Dispose() which both resets the holder and invalidates weak_factory_, and calls Dispose() from ClearInspectTool() so an outgoing tool can no longer be reached by a stale weak-bound task.
OnAgentDisable) while a second path (ClearInspectTool) left the tool orphaned but still weakly reachable; the fix routes both paths through Dispose(), which drops the session reference and invalidates the weak factory so no pending task can fire through a dangling pointer.Attack Path
- Trigger a debugger pause
Use the DevTools/inspector protocol to hit a
debuggerstatement soInspectorOverlayAgentinstalls aPausedInDebuggerToolholding the V8 session pointer. - Queue a session action
Send resume or stepOver, which schedules an
ExecuteOnV8Sessiontask bound to the tool through itsWeakCellFactorywhile the tool is still reachable. - Swap out the tool without disabling the agent
Cause
ClearInspectTool()to run so the old tool is orphaned; before the fix this did not clearv8_session_, so the pending weak-bound task still targets it. - Detach the session to free it
Disconnect so
DevToolsSession::Detach()destroys theV8InspectorSession, leavingv8_session_dangling. - Dangling virtual call executes
When the pending task runs, it performs a virtual call through the freed
v8_inspector::V8InspectorSessionpointer, a use-after-free that could be steered toward further corruption(inferred).
Impact Assessment
V8InspectorSession in the Blink renderer process, reachable by orchestrating a debugger pause, a queued resume/stepOver, a tool swap, and a session detach. Escalation from this dangling virtual call to a controlled read/write primitive or code execution is (inferred) and not demonstrated by the patch. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8.Changed Functions
| Function | Change | Notes |
|---|---|---|
message_third_party/blink/renderer/core/inspector/inspect_tools.h |
modified | |
ifthird_party/blink/renderer/core/inspector/inspector_overlay_agent.cc |
modified |
Files Changed
third_party/blink/renderer/core/inspector/inspect_tools.hthird_party/blink/renderer/core/inspector/inspector_overlay_agent.hthird_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect.jsthird_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect-expected.txtthird_party/blink/renderer/core/inspector/inspector_overlay_agent.ccthird_party/blink/renderer/core/inspector/inspect_tools.cc
Audit Directions
- Raw unowned pointers to detachable session objectsSearch for
raw_ptrfields markedDanglingUntriaged/UnprotectedInReleasethat holdV8InspectorSessionor other DevTools objects whose lifetime ends at session detach. - Asymmetric teardown pathsLook for objects cleared on only one lifecycle callback (e.g. disable) but reachable via a second path (swap/clear) that fails to run the same cleanup.
- Weak-bound pending tasksAudit
WeakCellFactory/weak-callback bindings where a scheduled task can outlive the object’s active role, especially when it dereferences an unowned raw pointer.
Patch
From 0ac1a9c535c64859d95ee4639ef1727aa2e01027 Mon Sep 17 00:00:00 2001 From: Alex Rudenko <[email protected]> Date: Thu, 27 Aug 2026 04:29:05 -0700 Subject: [PATCH] Disable overlay tools correctly Fixed: 553130481 Change-Id: Ia15cc2d7b7a93103b20c0f4b77455403b3ee32e9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8304033 Auto-Submit: Alex Rudenko <[email protected]> Reviewed-by: Simon Zünd <[email protected]> Commit-Queue: Simon Zünd <[email protected]> Cr-Commit-Position: refs/heads/main@{#1687096} --- diff --git a/third_party/blink/renderer/core/inspector/inspect_tools.cc b/third_party/blink/renderer/core/inspector/inspect_tools.cc index a36f7fb..41723fd 100644 --- a/third_party/blink/renderer/core/inspector/inspect_tools.cc +++ b/third_party/blink/renderer/core/inspector/inspect_tools.cc @@ -837,8 +837,9 @@ } } -void PausedInDebuggerTool::OnAgentDisable() { - v8_session_ = nullptr; +void PausedInDebuggerTool::Dispose() { + v8_session_.reset(); + weak_factory_.Invalidate(); } void PausedInDebuggerTool::Trace(Visitor* visitor) const { diff --git a/third_party/blink/renderer/core/inspector/inspect_tools.h b/third_party/blink/renderer/core/inspector/inspect_tools.h index 5261eee..fd326c3 100644 --- a/third_party/blink/renderer/core/inspector/inspect_tools.h +++ b/third_party/blink/renderer/core/inspector/inspect_tools.h @@ -13,6 +13,7 @@ #include "third_party/blink/renderer/bindings/core/v8/script_value.h" #include "third_party/blink/renderer/core/inspector/inspector_overlay_agent.h" #include "third_party/blink/renderer/core/inspector/node_content_visibility_state.h" +#include "third_party/blink/renderer/core/inspector/v8_session_holder.h" #include "third_party/blink/renderer/platform/heap/weak_cell.h" namespace blink { @@ -233,10 +234,10 @@ public: PausedInDebuggerTool(InspectorOverlayAgent* overlay, OverlayFrontend* frontend, - v8_inspector::V8InspectorSession* v8_session, + V8SessionHolder v8_session, const String& message) : InspectTool(overlay, frontend), - v8_session_(v8_session), + v8_session_(std::move(v8_session)), message_(message) {} PausedInDebuggerTool(const PausedInDebuggerTool&) = delete; PausedInDebuggerTool& operator=(const PausedInDebuggerTool&) = delete; @@ -249,12 +250,10 @@ void Dispatch(const ScriptValue& message, ExceptionState& exception_state) override; String GetOverlayName() override; - void OnAgentDisable() override; + void Dispose() override; void ExecuteOnV8Session(Action action); - raw_ptr<v8_inspector::V8InspectorSession, - UnprotectedInRelease | DanglingUntriaged> - v8_session_; + V8SessionHolder v8_session_; String message_; WeakCellFactory<PausedInDebuggerTool> weak_factory_{this}; }; diff --git a/third_party/blink/renderer/core/inspector/inspector_overlay_agent.cc b/third_party/blink/renderer/core/inspector/inspector_overlay_agent.cc index a0887146..20d5008c 100644 --- a/third_party/blink/renderer/core/inspector/inspector_overlay_agent.cc +++ b/third_party/blink/renderer/core/inspector/inspector_overlay_agent.cc @@ -649,7 +649,7 @@ hinge_ = nullptr; display_cutout_ = nullptr; if (inspect_tool_) { - inspect_tool_->OnAgentDisable(); + inspect_tool_->Dispose(); } PickTheRightTool(); SetNeedsUnbufferedInput(false); @@ -1847,7 +1847,7 @@ inspect_tool = MakeGarbageCollected<ScreenshotTool>(this, GetFrontend()); } else if (!paused_in_debugger_message_.empty()) { inspect_tool = MakeGarbageCollected<PausedInDebuggerTool>( - this, GetFrontend(), V8Session().get(), paused_in_debugger_message_); + this, GetFrontend(), V8Session(), paused_in_debugger_message_); } else if (persistent_tool_) { inspect_tool = persistent_tool_; } @@ -1880,6 +1880,15 @@ } void InspectorOverlayAgent::ClearInspectTool() { + if (inspect_tool_) { + // Notify the outgoing tool that it is being uninstalled so that it drops + // its unowned reference to the V8 inspector session. Otherwise a + // PausedInDebuggerTool orphaned here can still be reached by a pending + // ExecuteOnV8Session task (bound through its WeakCell) and perform a + // virtual call through a raw v8_inspector::V8InspectorSession pointer + // that dangles once DevToolsSession::Detach() destroys the session. + inspect_tool_->Dispose(); + } inspect_tool_ = nullptr; if (!hinge_ && !display_cutout_) { DisableFrameOverlay(); diff --git a/third_party/blink/renderer/core/inspector/inspector_overlay_agent.h b/third_party/blink/renderer/core/inspector/inspector_overlay_agent.h index 42683b46..de8bbcc 100644 --- a/third_party/blink/renderer/core/inspector/inspector_overlay_agent.h +++ b/third_party/blink/renderer/core/inspector/inspector_overlay_agent.h @@ -119,7 +119,7 @@ virtual void Trace(Visitor* visitor) const; virtual bool HideOnHideHighlight(); virtual bool HideOnMouseMove(); - virtual void OnAgentDisable() {} + virtual void Dispose() {} protected: Member<InspectorOverlayAgent> overlay_; diff --git a/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect-expected.txt b/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect-expected.txt new file mode 100644 index 0000000..d4ece75 --- /dev/null +++ b/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect-expected.txt @@ -0,0 +1,8 @@ +Verifies that clearing inspect tool and disconnecting session after sending resume/stepOver in overlay does not cause use-after-free. +Testing resume: +Paused on debugger statement. +Cleared inspect tool and disconnected session after resume. +Testing stepOver: +Paused on debugger statement. +Cleared inspect tool and disconnected session after stepOver. + diff --git a/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect.js b/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect.js new file mode 100644 index 0000000..ef6edcbb --- /dev/null +++ b/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect.js @@ -0,0 +1,64 @@ +(async function(/** @type {import('test_runner').TestRunner} */ testRunner) { + const {page, session, dp} = await testRunner.startHTML( + ` + <!DOCTYPE html> + <script> + function pause() { + debugger; + } + </script> + `, + 'Verifies that clearing inspect tool and disconnecting session after sending resume/stepOver in overlay does not cause use-after-free.'); + + testRunner.log('Testing resume:'); + await dp.DOM.enable(); + await dp.Overlay.enable(); + await dp.Debugger.enable(); + + dp.Runtime.evaluate({expression: 'pause()'}); + await dp.Debugger.oncePaused(); + await dp.Overlay.setPausedInDebuggerMessage( + {message: 'Paused on debugger statement'}); + testRunner.log('Paused on debugger statement.'); + + // Trigger 'resume' action via overlay host (posts ExecuteOnV8Session task) + // and immediately clear inspect tool and disconnect before the posted task + // executes. + session.evaluate(() => { + internals.evaluateInInspectorOverlay(`(function () { + window.InspectorOverlayHost.send('resume'); + })()`); + }); + dp.Overlay.setPausedInDebuggerMessage({}); + await session.disconnect(); + testRunner.log('Cleared inspect tool and disconnected session after resume.'); + + testRunner.log('Testing stepOver:'); + const session2 = await page.createSession(); + const dp2 = session2.protocol; + + await dp2.DOM.enable(); + await dp2.Overlay.enable(); + await dp2.Debugger.enable(); + + dp2.Runtime.evaluate({expression: 'pause()'}); + await dp2.Debugger.oncePaused(); + await dp2.Overlay.setPausedInDebuggerMessage( + {message: 'Paused on debugger statement'}); + testRunner.log('Paused on debugger statement.'); + + // Trigger 'stepOver' action via overlay host (posts ExecuteOnV8Session task) + // and immediately clear inspect tool and disconnect before the posted task + // executes. + session2.evaluate(() => { + internals.evaluateInInspectorOverlay(`(function () { + window.InspectorOverlayHost.send('stepOver'); + })()`); + }); + dp2.Overlay.setPausedInDebuggerMessage({}); + await session2.disconnect(); + testRunner.log( + 'Cleared inspect tool and disconnected session after stepOver.'); + + testRunner.completeTest(); +});
Regression Test / PoC
diff --git a/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect-expected.txt b/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect-expected.txt
new file mode 100644
index 0000000..d4ece75
--- /dev/null
+++ b/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect-expected.txt
@@ -0,0 +1,8 @@
+Verifies that clearing inspect tool and disconnecting session after sending resume/stepOver in overlay does not cause use-after-free.
+Testing resume:
+Paused on debugger statement.
+Cleared inspect tool and disconnected session after resume.
+Testing stepOver:
+Paused on debugger statement.
+Cleared inspect tool and disconnected session after stepOver.
+
diff --git a/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect.js b/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect.js
new file mode 100644
index 0000000..ef6edcbb
--- /dev/null
+++ b/third_party/blink/web_tests/inspector-protocol/overlay/overlay-paused-resume-disconnect.js
@@ -0,0 +1,64 @@
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+ const {page, session, dp} = await testRunner.startHTML(
+ `
+ <!DOCTYPE html>
+ <script>
+ function pause() {
+ debugger;
+ }
+ </script>
+ `,
+ 'Verifies that clearing inspect tool and disconnecting session after sending resume/stepOver in overlay does not cause use-after-free.');
+
+ testRunner.log('Testing resume:');
+ await dp.DOM.enable();
+ await dp.Overlay.enable();
+ await dp.Debugger.enable();
+
+ dp.Runtime.evaluate({expression: 'pause()'});
+ await dp.Debugger.oncePaused();
+ await dp.Overlay.setPausedInDebuggerMessage(
+ {message: 'Paused on debugger statement'});
+ testRunner.log('Paused on debugger statement.');
+
+ // Trigger 'resume' action via overlay host (posts ExecuteOnV8Session task)
+ // and immediately clear inspect tool and disconnect before the posted task
+ // executes.
+ session.evaluate(() => {
+ internals.evaluateInInspectorOverlay(`(function () {
+ window.InspectorOverlayHost.send('resume');
+ })()`);
+ });
+ dp.Overlay.setPausedInDebuggerMessage({});
+ await session.disconnect();
+ testRunner.log('Cleared inspect tool and disconnected session after resume.');
+
+ testRunner.log('Testing stepOver:');
+ const session2 = await page.createSession();
+ const dp2 = session2.protocol;
+
+ await dp2.DOM.enable();
+ await dp2.Overlay.enable();
+ await dp2.Debugger.enable();
+
+ dp2.Runtime.evaluate({expression: 'pause()'});
+ await dp2.Debugger.oncePaused();
+ await dp2.Overlay.setPausedInDebuggerMessage(
+ {message: 'Paused on debugger statement'});
+ testRunner.log('Paused on debugger statement.');
+
+ // Trigger 'stepOver' action via overlay host (posts ExecuteOnV8Session task)
+ // and immediately clear inspect tool and disconnect before the posted task
+ // executes.
+ session2.evaluate(() => {
+ internals.evaluateInInspectorOverlay(`(function () {
+ window.InspectorOverlayHost.send('stepOver');
+ })()`);
+ });
+ dp2.Overlay.setPausedInDebuggerMessage({});
+ await session2.disconnect();
+ testRunner.log(
+ 'Cleared inspect tool and disconnected session after stepOver.');
+
+ testRunner.completeTest();
+});
Original Bug Report
Use-After-Free in PausedInDebuggerTool in Blink Inspector Overlay
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 Use-After-Free (UAF) exists in Blink’s inspector overlay due to missing cleanup when switching or clearing inspect tools. PausedInDebuggerTool retains an UnprotectedInRelease raw pointer to v8_inspector::V8InspectorSession, bypassing MiraclePtr in release builds. If the tool is replaced while a task is pending on the kInternalInspector queue and DevTools随后 detaches, the pending task makes a virtual method call on the freed session object.
Affected files:
third_party/blink/renderer/core/inspector/inspector_overlay_agent.ccthird_party/blink/renderer/core/inspector/inspect_tools.ccthird_party/blink/renderer/core/inspector/inspect_tools.h
Estimated timestamp from git blame: 2025-12-08
Summary
A potential Use-After-Free vulnerability exists in the Blink inspector overlay implementation (third_party/blink/renderer/core/inspector/). When inspect tools are replaced or cleared via InspectorOverlayAgent::ClearInspectTool(), the outgoing InspectTool is orphaned without invoking its OnAgentDisable() cleanup hook or invalidating its WeakCellFactory. Consequently, a pending ExecuteOnV8Session task can execute virtual method calls against a freed v8_inspector::V8InspectorSessionImpl instance after DevTools detaches.
Root Cause Analysis
- Unprotected Raw Pointer in
PausedInDebuggerTool: Inthird_party/blink/renderer/core/inspector/inspect_tools.h(lines 255–259):
raw_ptr<v8_inspector::V8InspectorSession,
UnprotectedInRelease | DanglingUntriaged>
v8_session_;
String message_;
WeakCellFactory<PausedInDebuggerTool> weak_factory_{this};
Because v8_session_ is annotated with UnprotectedInRelease, it uses a no-op implementation in release builds, providing no MiraclePtr (BackupRefPtr) quarantine or protection.
When PausedInDebuggerTool is constructed in InspectorOverlayAgent::PickTheRightTool() (third_party/blink/renderer/core/inspector/inspector_overlay_agent.cc:1848–1850), it receives only a raw pointer extracted from a temporary by-value V8SessionHolder (V8Session().get()), holding no shared ownership over the session.
- Tool Orphaning Without Invalidation:
When
PausedInDebuggerTool::Dispatch()processes a user click on the overlay pause banner (third_party/blink/renderer/core/inspector/inspect_tools.cc:804–810), it posts anExecuteOnV8Sessiontask toTaskType::kInternalInspectorusing aPersistent-wrappedWeakCell:
task_runner->PostTask(
FROM_HERE,
blink::BindOnce(&PausedInDebuggerTool::ExecuteOnV8Session,
WrapPersistent(weak_factory_.GetWeakCell()),
Action::kResume));
If another CDP command updates the tool (e.g. setPausedInDebuggerMessage, setInspectMode, highlightNode), InspectorOverlayAgent::ClearInspectTool() is called (inspector_overlay_agent.cc:1882–1886):
void InspectorOverlayAgent::ClearInspectTool() {
inspect_tool_ = nullptr;
if (!hinge_ && !display_cutout_) {
DisableFrameOverlay();
}
}
ClearInspectTool() does not invoke inspect_tool_->OnAgentDisable() on the outgoing tool, nor does it call weak_factory_.Invalidate(). The only call to OnAgentDisable() occurs in InspectorOverlayAgent::disable() (inspector_overlay_agent.cc:651–653), which only reaches whichever tool is currently assigned to inspect_tool_.
-
Synchronous Free upon DevTools Detach: When DevTools detaches while paused,
DevToolsSession::Detach()(third_party/blink/renderer/core/inspector/devtools_session.cc:272–291) disposes all agents and then drops the session viav8_session_.reset(), synchronously destroying theV8InspectorSessionImplinstance (v8/src/inspector/v8-inspector-session-impl.cc:164–173). -
The UAF Sink: When the queued
ExecuteOnV8Sessiontask executes from the unpausableTaskType::kInternalInspectorqueue:
void PausedInDebuggerTool::ExecuteOnV8Session(Action action) {
if (!v8_session_) {
return;
}
switch (action) {
case Action::kResume:
v8_session_->resume(/* setTerminateOnResume */ false);
break;
case Action::kStepOver:
v8_session_->stepOver();
break;
}
}
Because v8_session_ was never reset to nullptr on the orphaned tool instance, v8_session_->resume() or v8_session_->stepOver() is called. These methods are pure virtual functions (v8/include/v8-inspector.h:197–198), resulting in a virtual table dereference and indirect call through the freed heap object.
Potential Reproduction Steps
(Note: These are analytically derived steps; code execution has not been verified with a live PoC.)
- Attach DevTools to an attacker-controlled webpage where the Overlay domain is enabled.
- Trigger a debugger pause from script (e.g. via a
debugger;statement). - DevTools frontend sends
Overlay.setPausedInDebuggerMessage, instantiating and installingPausedInDebuggerTool($T_1$). - User clicks ‘Resume’ or ‘Step Over’ on the pause banner. This enqueues
ExecuteOnV8Sessionto the frame’sTaskType::kInternalInspectorqueue, bound to $T_1$. - DevTools processes a tool-replacing CDP command (such as updating the pause message or enabling inspect mode), causing
SetInspectTool->ClearInspectToolto orphan $T_1$ while itsv8_session_remains non-null. - DevTools is closed, invoking
DevToolsSession::Detach(). The active agents are disposed (only the active tool hasOnAgentDisable()called) andv8_session_.reset()destroysV8InspectorSessionImpl. - The pending
kInternalInspectortask fires.weak_cell->Get()resolves to $T_1$, andv8_session_->resume()triggers virtual dispatch on the freed memory.
Suggested Fix
- In
InspectorOverlayAgent::ClearInspectTool(), invokeOnAgentDisable()oninspect_tool_before setting it tonullptr:
void InspectorOverlayAgent::ClearInspectTool() {
if (inspect_tool_) {
inspect_tool_->OnAgentDisable();
}
inspect_tool_ = nullptr;
if (!hinge_ && !display_cutout_) {
DisableFrameOverlay();
}
}
- Additionally, consider having
PausedInDebuggerToolhold aV8SessionHolder(std::shared_ptr<v8_inspector::V8InspectorSession>) directly, or invalidateweak_factory_insideOnAgentDisable().
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.