Chrome · DevTools › Inspector
CVE-2026-2319
Race in DevTools (Inspector)
Overview
Medium
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
Background
- `V8InspectorSession`
- the V8-side object for one DevTools protocol session; it owns the V8 domain agents and the
m_dispatcherthat routes Runtime/Debugger/Profiler/… commands. - `DevToolsSession`
- the Blink garbage-collected object that bridges a browser DevTools connection to Blink agents and to its
v8_session_. - Nested message loop on pause
- while JS is paused in the debugger, V8 asks the embedder to run a nested loop (
V8InspectorClient::runMessageLoopOnPause, per the V8 header comment in the diff), so other tasks can run while the protocol call that caused the pause is still on the stack. - `connectShared`
- a new
V8InspectorAPI that returns astd::shared_ptr<V8InspectorSession>, which lets V8 keep the session alive while it is in use.
Root Cause Analysis
Re-analyzed on 2026-10-03 against the actual fix commit
531984a74b5f. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (73b918cfcdb3).Before the fix,
DevToolsSession::ConnectToV8 got the session from inspector->connect(...) and stored it in std::unique_ptr<v8_inspector::V8InspectorSession> v8_session_ (devtools_session.cc:193, devtools_session.h:174), so Blink was the only owner. DevToolsSession::Detach, which is also the host_remote_ disconnect handler (devtools_session.cc:173-174), destroyed the session immediately with v8_session_.reset() (devtools_session.cc:227). Protocol commands reach V8 through v8_session_->dispatchProtocolMessage(...) (devtools_session.cc:274), which ends in m_dispatcher.Dispatch(dispatchable).Run() (v8-inspector-session-impl.cc:395). The V8 commit message says “blink is allowed to tear down V8 sessions on the nested run-loop, while actual agent/session functions remain on the stack”, so a Detach that runs inside a nested pause loop frees the V8InspectorSessionImpl under the outer dispatch frames. This breaks the invariant that a session must not be destroyed while one of its own methods is on the stack; the bug comes from task ordering (hence the advisory’s “Race” label) and shows up as a use-after-free. Prerequisite V8 commit 73b918cf adds connectShared, which calls session->setWeakThis(session), and puts KeepSessionAliveScope keepAlive(*this); at the top of dispatchProtocolMessage to hold a strong reference for the whole dispatch. Primary Blink commit 531984a7 switches ConnectToV8 to connectShared and v8_session_ to std::shared_ptr. Without that switch m_weakThis is never set and the scope protects nothing; with it, reset() only drops Blink’s reference. The same commit adds v8_session_->stop(); before the reset, because (per the reland message) the destructor used to resume a paused session, and once destruction is deferred the nested loop must be ended explicitly so the stack can unwind.Key insight
Blink exclusively owned a V8 session that could be destroyed from a nested loop while its own
dispatchProtocolMessage was still on the stack. The fix changes ownership to shared, makes each dispatch hold a strong reference, and stops the session explicitly on detach so the deferred teardown still completes.Attack Path
- Attach a DevTools session
A DevTools client attaches to a renderer, and
ConnectToV8creates the V8 session (devtools_session.cc:190-201). - Enter a nested pause loop from a dispatched command
A V8-domain command goes through
dispatchProtocolMessage(devtools_session.cc:274,v8-inspector-session-impl.cc:395) and leads to a debugger pause that runs a nested message loop (inferred from the V8 commit message). - Detach while nested
The session is detached, for example when the host pipe disconnects and
Detachruns (devtools_session.cc:173-174).v8_session_.reset()then frees the session (devtools_session.cc:227). That the disconnect is processed inside the nested loop is (inferred). - Unwind into freed state
When the nested loop returns, the outer dispatch frames continue using the freed
V8InspectorSessionImpland its dispatcher/agents. The reporter attributes the crash toDomainDispatcherImpl(reporter’s claim; that frame is not in the material).
Impact Assessment
The result is a use-after-free in the renderer process’s V8 inspector. The reporter calls it a “renderer UAF”, and the advisory rates it medium, not exploited in the wild. A DevTools/debugger session must be attached and then detached while paused. How far an untrusted party can control this sequence, and whether it leads to code execution, is (inferred) and not shown in the material.
Files Changed
PRESUBMIT.pythird_party/blink/renderer/core/inspector/devtools_session.ccthird_party/blink/renderer/core/inspector/devtools_session.h
Audit Directions
- Other nested-loop entry pointsThe V8 commit message says that client events and interrupts can also enter agent code and start a nested loop without passing through
dispatchProtocolMessage. Check those paths for a missing keep-alive. - Raw pointers to shared-owned sessions
DevToolsSession::V8Session()still returnsv8_session_.get()(devtools_session.h:77). Check whether any caller caches that raw pointer across a point where the session can be detached. - Owner reset during re-entrancyLook for other
unique_ptr::reset()calls in teardown handlers such as Mojo disconnect handlers that can run in a nested run loop while a method of the owned object is still on the stack.
Patch
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 9147703..01abdfd 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -748,6 +748,7 @@
# Needed for interop with third-party library.
r'^third_party/blink/renderer/core/typed_arrays/array_buffer/' +
r'array_buffer_contents\.(cc|h)',
+ r'^third_party/blink/renderer/core/inspector/devtools_session\.h',
r'^third_party/blink/renderer/core/typed_arrays/dom_array_buffer\.cc',
'^third_party/blink/renderer/bindings/core/v8/' +
'v8_wasm_response_extensions.cc',
diff --git a/third_party/blink/renderer/core/inspector/devtools_session.cc b/third_party/blink/renderer/core/inspector/devtools_session.cc
index 2a92b6d..2455ee368 100644
--- a/third_party/blink/renderer/core/inspector/devtools_session.cc
+++ b/third_party/blink/renderer/core/inspector/devtools_session.cc
@@ -190,7 +190,7 @@
void DevToolsSession::ConnectToV8(v8_inspector::V8Inspector* inspector,
int context_group_id) {
const auto& cbor = v8_session_state_cbor_.Get();
- v8_session_ = inspector->connect(
+ v8_session_ = inspector->connectShared(
context_group_id, this,
v8_inspector::StringView(cbor.data(), cbor.size()),
client_is_trusted_ ? v8_inspector::V8Inspector::kFullyTrusted
@@ -224,6 +224,7 @@
for (wtf_size_t i = agents_.size(); i > 0; i--)
agents_[i - 1]->Dispose();
agents_.clear();
+ v8_session_->stop();
v8_session_.reset();
agent_->client_->DebuggerTaskFinished();
}
diff --git a/third_party/blink/renderer/core/inspector/devtools_session.h b/third_party/blink/renderer/core/inspector/devtools_session.h
index 4ee2af4e..133b4ff 100644
--- a/third_party/blink/renderer/core/inspector/devtools_session.h
+++ b/third_party/blink/renderer/core/inspector/devtools_session.h
@@ -171,7 +171,7 @@
HeapMojoAssociatedRemote<mojom::blink::DevToolsSessionHost> host_remote_{
nullptr};
IOSession* io_session_;
- std::unique_ptr<v8_inspector::V8InspectorSession> v8_session_;
+ std::shared_ptr<v8_inspector::V8InspectorSession> v8_session_;
std::unique_ptr<protocol::UberDispatcher> inspector_backend_dispatcher_;
InspectorSessionState session_state_;
HeapVector<Member<InspectorAgent>> agents_;
Loading diff…
Original Bug Report
reported by he...@gmail.com
UAF in v8_inspector DomainDispatcherImpl
Steps to reproduce the problem:
will attach details soon.
Problem Description:
renderer UAF
Additional Comments:
**Chrome version: ** 118.0.5979.0 **Channel: ** Dev
OS: Linux
References
On This Page