CVE-2026-87617
Overview
Background
- `InspectedContext`
- The V8 inspector object that owns the per-context inspector state, including the
InjectedScriptinstance, and is reference-counted viastd::shared_ptr. - `InjectedScript`
- The object that wraps and serializes JavaScript values into DevTools protocol
RemoteObjects; it is owned by (and lives no longer than) itsInspectedContext. - `ConsoleHelper`
- A short-lived helper built around a console call that resolves the session, injected script, and context needed to service CommandLineAPI functions such as
inspect. - CommandLineAPI `inspect`
- A console-only helper function that reveals an object in DevTools; internally it wraps the argument through
InjectedScript, which can re-enter user JavaScript during serialization.
Root Cause Analysis
ConsoleHelper::injectedScript() obtained the InspectedContext via m_inspector->getContext(...) into a function-local std::shared_ptr<InspectedContext>, then returned the raw InjectedScript* owned by that context. Because the local shared_ptr was destroyed when the function returned, nothing in ConsoleHelper held a strong reference to the context for the remainder of the inspect operation. When the captured CommandLineAPI inspect function was invoked from ordinary page script — outside any active ContextScope that would otherwise pin the context — object serialization inside InjectedScript::wrapObjectMirror could re-enter JavaScript (e.g. a property getter) that synchronously destroyed the context, freeing the InjectedScript while it was still being used, producing a use-after-free.
The fix stores the shared_ptr in a new ConsoleHelper member m_inspectedContext, so the InspectedContext and its owned InjectedScript remain alive for the whole duration of inspectImpl execution.
InjectedScript unprotected against synchronous context destruction during serialization. Promoting the shared_ptr to the ConsoleHelper member m_inspectedContext keeps the owning InspectedContext pinned across the re-entrant serialization, closing the UAF window.Attack Path
- Enable inspector and capture `inspect`
With an inspector session and
Runtimedomain enabled, evaluate an expression withincludeCommandLineAPIto save the CommandLineAPIinspectfunction to a page-reachable global (globalThis.savedInspect = inspect). - Invoke from page scope
Call
savedInspect(obj)from ordinary page-level script execution, where noContextScopeholds theInspectedContextalive, soConsoleHelper::injectedScript()’s dropped localshared_ptris the only thing that had pinned it. - Re-enter during serialization
Pass an object whose serialization triggers a JavaScript getter (the PoC redefines
Error.prototype.namewith a getter) that runs duringInjectedScript::wrapObjectMirror. - Destroy the context synchronously
Inside that getter, fire context destruction (
inspector.fireContextDestroyed()), which frees theInspectedContextand its ownedInjectedScriptwhile the wrap call is still executing. - Use the freed object
Control returns into
wrapObjectMirroroperating on the now-freedInjectedScript, yielding the use-after-free; further escalation to a controlled read/write or code execution is not shown by the patch(inferred).
Impact Assessment
InjectedScript/InspectedContext in the V8 inspector, reachable whenever DevTools/an inspector client is attached with Runtime enabled and page script can capture and re-invoke the CommandLineAPI inspect function. The bug executes in the process hosting V8 inspection — the renderer process for Chrome DevTools (inferred) — and the preconditions are an active inspector session plus attacker-controlled page JavaScript. The advisory classifies this as a use-after-free in DevTools; any escalation to memory disclosure or code execution beyond the crash is (inferred) and not demonstrated by the patch, and no in-the-wild exploitation is stated.Changed Functions
| Function | Change | Notes |
|---|---|---|
gettest/inspector/runtime/regress-523442920.js |
modified |
Files Changed
test/inspector/runtime/regress-523442920-expected.txttest/inspector/runtime/regress-523442920.jssrc/inspector/v8-console.cc
Audit Directions
- Raw pointer outliving its owning smart pointerFlag functions that fetch an owner via a local
shared_ptr/scoped_refptrand return or expose a raw pointer/reference into it; the callee must hold a strong reference for as long as it uses the object. - Re-entrant serialization sinksAudit
InjectedScript/mirror wrapping paths (wrapObjectMirror,wrapObject, RemoteObject generation) for points that invoke user JavaScript (getters,toString, proxies) which can synchronously destroy the context or session being used. - CommandLineAPI functions invoked outside a `ContextScope`Review all console/CommandLineAPI helpers that can be captured and called from page script, checking that each pins its
InspectedContextrather than relying on a caller-established scope.
Patch
From 23294e43bec49915a9f3fb9f3b45acc559d1bd6e Mon Sep 17 00:00:00 2001
From: Yang Guo <yangguo@chromium.org>
Date: Mon, 10 Aug 2026 05:11:17 +0000
Subject: [PATCH] [inspector] Pin InspectedContext in ConsoleHelper to prevent UAF
ConsoleHelper::injectedScript() previously dropped its local
shared_ptr<InspectedContext> before returning the raw InjectedScript*
pointer. If the CommandLineAPI inspect function was captured and invoked
from a page script outside an active ContextScope, synchronous context
destruction during object serialization caused a use-after-free in
InjectedScript::wrapObjectMirror.
Keep a std::shared_ptr<InspectedContext> in ConsoleHelper so the
context remains pinned during inspectImpl execution.
TAG=agy
CONV=812b1007-2667-4bf3-8c0b-7669dff6da64
Fixed: 523442920
Change-Id: I96dcd0c3c04b02d3ab3a92a0a3dbb6a0876b25e8
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8225404
Commit-Queue: Simon Zünd <szuend@chromium.org>
Auto-Submit: Yang Guo <yangguo@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
Cr-Commit-Position: refs/heads/main@{#109151}
---
diff --git a/src/inspector/v8-console.cc b/src/inspector/v8-console.cc
index 0fa7caf..0ad21a5 100644
--- a/src/inspector/v8-console.cc
+++ b/src/inspector/v8-console.cc
@@ -67,10 +67,9 @@
int groupId() const { return m_inspector->contextGroupId(contextId()); }
InjectedScript* injectedScript(int sessionId) {
- std::shared_ptr<InspectedContext> context =
- m_inspector->getContext(groupId(), contextId());
- if (!context) return nullptr;
- return context->getInjectedScript(sessionId);
+ m_inspectedContext = m_inspector->getContext(groupId(), contextId());
+ if (!m_inspectedContext) return nullptr;
+ return m_inspectedContext->getInjectedScript(sessionId);
}
V8InspectorSessionImpl* session(int sessionId) {
@@ -210,6 +209,7 @@
const v8::debug::ConsoleCallArguments& m_info;
const v8::debug::ConsoleContext& m_consoleContext;
V8InspectorImpl* m_inspector;
+ std::shared_ptr<InspectedContext> m_inspectedContext;
};
void createBoundFunctionProperty(
diff --git a/test/inspector/runtime/regress-523442920-expected.txt b/test/inspector/runtime/regress-523442920-expected.txt
new file mode 100644
index 0000000..ac306d2
--- /dev/null
+++ b/test/inspector/runtime/regress-523442920-expected.txt
@@ -0,0 +1,2 @@
+Tests that destroying context during inspect does not cause UAF (regress-523442920).
+Success (no crash).
diff --git a/test/inspector/runtime/regress-523442920.js b/test/inspector/runtime/regress-523442920.js
new file mode 100644
index 0000000..d3442cd
--- /dev/null
+++ b/test/inspector/runtime/regress-523442920.js
@@ -0,0 +1,32 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+let {session, contextGroup, Protocol} =
+ InspectorTest.start('Tests that destroying context during inspect does not cause UAF (regress-523442920).');
+
+(async function test() {
+ await Protocol.Runtime.enable();
+
+ // Capture CommandLineAPI inspect function.
+ await Protocol.Runtime.evaluate({
+ expression: 'globalThis.savedInspect = inspect;',
+ includeCommandLineAPI: true,
+ });
+
+ // Call savedInspect from page-level execution where no ContextScope is active.
+ contextGroup.addScript(`
+ let e = new Error();
+ delete e.name;
+ Object.defineProperty(Object.getPrototypeOf(e), 'name', {
+ get() {
+ inspector.fireContextDestroyed();
+ return '';
+ }
+ });
+ savedInspect(e);
+ `);
+
+ InspectorTest.log('Success (no crash).');
+ InspectorTest.completeTest();
+})();
Regression Test / PoC
diff --git a/test/inspector/runtime/regress-523442920-expected.txt b/test/inspector/runtime/regress-523442920-expected.txt
new file mode 100644
index 0000000..ac306d2
--- /dev/null
+++ b/test/inspector/runtime/regress-523442920-expected.txt
@@ -0,0 +1,2 @@
+Tests that destroying context during inspect does not cause UAF (regress-523442920).
+Success (no crash).
diff --git a/test/inspector/runtime/regress-523442920.js b/test/inspector/runtime/regress-523442920.js
new file mode 100644
index 0000000..d3442cd
--- /dev/null
+++ b/test/inspector/runtime/regress-523442920.js
@@ -0,0 +1,32 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+let {session, contextGroup, Protocol} =
+ InspectorTest.start('Tests that destroying context during inspect does not cause UAF (regress-523442920).');
+
+(async function test() {
+ await Protocol.Runtime.enable();
+
+ // Capture CommandLineAPI inspect function.
+ await Protocol.Runtime.evaluate({
+ expression: 'globalThis.savedInspect = inspect;',
+ includeCommandLineAPI: true,
+ });
+
+ // Call savedInspect from page-level execution where no ContextScope is active.
+ contextGroup.addScript(`
+ let e = new Error();
+ delete e.name;
+ Object.defineProperty(Object.getPrototypeOf(e), 'name', {
+ get() {
+ inspector.fireContextDestroyed();
+ return '';
+ }
+ });
+ savedInspect(e);
+ `);
+
+ InspectorTest.log('Success (no crash).');
+ InspectorTest.completeTest();
+})();