CVE-2026-10989
Overview
Background
- Command Line API
- the set of debugging helper globals (e.g.
$0,$_,inspect) that V8’s inspector injects into the evaluation scope when a CDPRuntime.evaluate/Debugger.evaluateOnCallFramecall passesincludeCommandLineAPI: true. - `InjectedScript::CallFrameScope` / `scope.installCommandLineAPI()`
- the RAII scope used while evaluating on a paused frame;
installCommandLineAPI()writes the helper properties onto the global/context object. - `scope.tryCatch()`
- the
v8::TryCatchembedded in the scope that captures any JavaScript exception raised by V8 operations performed inside the scope. - Frozen global + strict frame
- after
Object.freeze(globalThis), writing a property to the global object throwsTypeErrorunder strict-mode semantics rather than silently failing as it does in sloppy mode.
Root Cause Analysis
In V8DebuggerAgentImpl::evaluateOnCallFrame (in v8-debugger-agent-impl.cc) and V8RuntimeAgentImpl::evaluate (in v8-runtime-agent-impl.cc), the pre-patch code called scope.installCommandLineAPI() unconditionally when includeCommandLineAPI was set and never inspected the result. When the global object is frozen and a strict-mode frame is on the stack, installing the helper properties triggers a TypeError inside V8, leaving a caught/pending exception in the scope’s tryCatch() that the code then failed to observe. Execution proceeded past the failed installation into the rest of the evaluation path with an inconsistent inspector state, which is the “inappropriate implementation” the advisory describes (the regression test title asserts it “does not crash”).
The fix adds if (scope.tryCatch().HasCaught()) immediately after installCommandLineAPI() and returns a CDP Response::ServerError("Failed to install command line API") (or callback->sendFailure(...) in the runtime agent) before any further evaluation runs. Because the check executes before the subsequent ignoreExceptionsAndMuteConsole()/evaluation steps, the failing call is now aborted cleanly instead of continuing in a corrupt state. The regression test confirms the strict frame now yields the -32000 Failed to install command line API error while the sloppy frame (where the write silently no-ops) still returns 4.
installCommandLineAPI() and ignoring that it can raise a JavaScript exception; the fix inspects scope.tryCatch().HasCaught() right after the call and gracefully fails the CDP request instead of proceeding with an unhandled pending exception.Attack Path
- Attach a debugger session
A client with CDP access (DevTools, or an attacker able to drive the inspector protocol) enables
Debugger/Runtimeon a target realm. - Freeze the global under strict mode
Script runs
"use strict"; Object.freeze(globalThis)and hits adebuggerbreakpoint so a strict-mode frame is paused on the stack. - Evaluate with the Command Line API
The client issues
Runtime.evaluate/Debugger.evaluateOnCallFramewithincludeCommandLineAPI: true, causinginstallCommandLineAPI()to attempt property writes on the frozen global. - Trigger the unchecked exception
The strict-mode write raises
TypeError; pre-patch, the caught exception is not observed and evaluation continues in an inconsistent state (inferred: leads to the crash/misbehavior noted by the regression test), rather than returning a clean CDP error.
Impact Assessment
LogicError / “Inappropriate implementation in V8,” with the regression test framed around avoiding a crash. Preconditions are an active inspector session and the ability to pause on a strict-mode frame with a frozen global object; the effect is confined to the V8 inspector path and requires debugging to be enabled. No memory-corruption or code-execution primitive is demonstrated by the patch (any escalation beyond the logic/stability fault is not established).Files Changed
src/inspector/v8-debugger-agent-impl.ccsrc/inspector/v8-runtime-agent-impl.cctest/inspector/regress/regress-crbug-516311623-expected.txttest/inspector/regress/regress-crbug-516311623.js
Audit Directions
- Unchecked inspector JS operationsSearch inspector code for calls that execute JavaScript or mutate context objects (property installs, formatter setup) that do not consult
tryCatch().HasCaught()before continuing. - Strict vs sloppy divergenceReview paths whose behavior silently no-ops in sloppy mode but throws in strict mode (writes to frozen/non-writable objects), and confirm both outcomes are handled.
- RAII scope error propagationAudit
CallFrameScope/ObjectScope/ContextScopeconsumers to ensure every operation that can leave a pending exception is checked before the scope’s remaining steps run.
Patch
From 772a0f365b170021dd9ab2c6549a82c4a18501aa Mon Sep 17 00:00:00 2001 From: Simon Zünd <[email protected]> Date: Tue, 26 May 2026 11:08:46 +0000 Subject: [PATCH] [inspector] Report a CDP error when installCommandLineAPI fails Trying to install the command line API function fails on a frozen globalThis with strict mode frames on the stack. We should gracefully fail the CDP call in this case. [email protected] Fixed: 516311623 Change-Id: Ib7b731f2d208ca2ccf932528fb3a9cf9b4ad9af5 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7876001 Reviewed-by: Kim-Anh Tran <[email protected]> Commit-Queue: Simon Zünd <[email protected]> Cr-Commit-Position: refs/heads/main@{#107558} --- diff --git a/src/inspector/v8-debugger-agent-impl.cc b/src/inspector/v8-debugger-agent-impl.cc index cc5b275..b95f582 100644 --- a/src/inspector/v8-debugger-agent-impl.cc +++ b/src/inspector/v8-debugger-agent-impl.cc @@ -1725,7 +1725,13 @@ InjectedScript::CallFrameScope scope(m_session, callFrameId); Response response = scope.initialize(); if (!response.IsSuccess()) return response; - if (includeCommandLineAPI.value_or(false)) scope.installCommandLineAPI(); + + if (includeCommandLineAPI.value_or(false)) { + scope.installCommandLineAPI(); + if (scope.tryCatch().HasCaught()) { + return Response::ServerError("Failed to install command line API"); + } + } if (silent.value_or(false)) scope.ignoreExceptionsAndMuteConsole(); int frameOrdinal = static_cast<int>(scope.frameOrdinal()); diff --git a/src/inspector/v8-runtime-agent-impl.cc b/src/inspector/v8-runtime-agent-impl.cc index b8d6d44..e6ce30c 100644 --- a/src/inspector/v8-runtime-agent-impl.cc +++ b/src/inspector/v8-runtime-agent-impl.cc @@ -386,7 +386,14 @@ if (silent.value_or(false)) scope.ignoreExceptionsAndMuteConsole(); if (userGesture.value_or(false)) scope.pretendUserGesture(); - if (includeCommandLineAPI.value_or(false)) scope.installCommandLineAPI(); + if (includeCommandLineAPI.value_or(false)) { + scope.installCommandLineAPI(); + if (scope.tryCatch().HasCaught()) { + callback->sendFailure( + Response::ServerError("Failed to install command line API")); + return; + } + } const bool replMode = maybeReplMode.value_or(false); diff --git a/test/inspector/regress/regress-crbug-516311623-expected.txt b/test/inspector/regress/regress-crbug-516311623-expected.txt new file mode 100644 index 0000000..717d571 --- /dev/null +++ b/test/inspector/regress/regress-crbug-516311623-expected.txt @@ -0,0 +1,24 @@ +Test that Runtime.evaluate with includeCommandLineAPI does not crash when global object is frozen. +Running test: testStrictFrame +Paused in strict mode. +Evaluating with includeCommandLineAPI while paused in strict... +{ + error : { + code : -32000 + message : Failed to install command line API + } + id : <messageId> +} +Running test: testSloppyFrame +Paused in sloppy mode. +Evaluating with includeCommandLineAPI while paused in sloppy... +{ + id : <messageId> + result : { + result : { + description : 4 + type : number + value : 4 + } + } +} diff --git a/test/inspector/regress/regress-crbug-516311623.js b/test/inspector/regress/regress-crbug-516311623.js new file mode 100644 index 0000000..b083cd4 --- /dev/null +++ b/test/inspector/regress/regress-crbug-516311623.js @@ -0,0 +1,72 @@ +// 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. + +const {session, Protocol} = InspectorTest.start( + 'Test that Runtime.evaluate with includeCommandLineAPI does not crash when global object is frozen.'); + +InspectorTest.runAsyncTestSuite([ + async function testStrictFrame() { + await Protocol.Debugger.enable(); + + // Define both strict and sloppy mode functions upfront before freezing the global object. + await Protocol.Runtime.evaluate({ + expression: ` + const global = this; + function runStrict() { + "use strict"; + Object.freeze(global); + debugger; + } + function runSloppy() { + debugger; + } + ` + }); + + // Run the strict function. It should pause. + const pausedPromise = Protocol.Debugger.oncePaused(); + Protocol.Runtime.evaluate({ expression: 'runStrict()' }); + await pausedPromise; + InspectorTest.log('Paused in strict mode.'); + + // Evaluate with includeCommandLineAPI: true. + // In strict mode, the command line API installation throws TypeError. + // The evaluation aborts early and returns the TypeError object. + InspectorTest.log('Evaluating with includeCommandLineAPI while paused in strict...'); + const evalResponse = await Protocol.Runtime.evaluate({ + expression: '2 + 2', + includeCommandLineAPI: true + }); + + // Log the full response to see the error. + InspectorTest.logMessage(evalResponse); + + await Protocol.Debugger.resume(); + }, + + async function testSloppyFrame() { + // Note: Global is already frozen from the previous test. + // runSloppy is also already defined. + + // Run the sloppy function. It should pause. + const pausedPromise = Protocol.Debugger.oncePaused(); + Protocol.Runtime.evaluate({ expression: 'runSloppy()' }); + await pausedPromise; + InspectorTest.log('Paused in sloppy mode.'); + + // Evaluate with includeCommandLineAPI: true. + // In sloppy mode, the command line API installation fails silently. + // The evaluation proceeds and returns 4. + InspectorTest.log('Evaluating with includeCommandLineAPI while paused in sloppy...'); + const evalResponse = await Protocol.Runtime.evaluate({ + expression: '2 + 2', + includeCommandLineAPI: true + }); + + // Log the full response, should be value: 4. + InspectorTest.logMessage(evalResponse); + + await Protocol.Debugger.resume(); + } +]);
Regression Test / PoC
diff --git a/test/inspector/regress/regress-crbug-516311623-expected.txt b/test/inspector/regress/regress-crbug-516311623-expected.txt
new file mode 100644
index 0000000..717d571
--- /dev/null
+++ b/test/inspector/regress/regress-crbug-516311623-expected.txt
@@ -0,0 +1,24 @@
+Test that Runtime.evaluate with includeCommandLineAPI does not crash when global object is frozen.
+Running test: testStrictFrame
+Paused in strict mode.
+Evaluating with includeCommandLineAPI while paused in strict...
+{
+ error : {
+ code : -32000
+ message : Failed to install command line API
+ }
+ id : <messageId>
+}
+Running test: testSloppyFrame
+Paused in sloppy mode.
+Evaluating with includeCommandLineAPI while paused in sloppy...
+{
+ id : <messageId>
+ result : {
+ result : {
+ description : 4
+ type : number
+ value : 4
+ }
+ }
+}
diff --git a/test/inspector/regress/regress-crbug-516311623.js b/test/inspector/regress/regress-crbug-516311623.js
new file mode 100644
index 0000000..b083cd4
--- /dev/null
+++ b/test/inspector/regress/regress-crbug-516311623.js
@@ -0,0 +1,72 @@
+// 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.
+
+const {session, Protocol} = InspectorTest.start(
+ 'Test that Runtime.evaluate with includeCommandLineAPI does not crash when global object is frozen.');
+
+InspectorTest.runAsyncTestSuite([
+ async function testStrictFrame() {
+ await Protocol.Debugger.enable();
+
+ // Define both strict and sloppy mode functions upfront before freezing the global object.
+ await Protocol.Runtime.evaluate({
+ expression: `
+ const global = this;
+ function runStrict() {
+ "use strict";
+ Object.freeze(global);
+ debugger;
+ }
+ function runSloppy() {
+ debugger;
+ }
+ `
+ });
+
+ // Run the strict function. It should pause.
+ const pausedPromise = Protocol.Debugger.oncePaused();
+ Protocol.Runtime.evaluate({ expression: 'runStrict()' });
+ await pausedPromise;
+ InspectorTest.log('Paused in strict mode.');
+
+ // Evaluate with includeCommandLineAPI: true.
+ // In strict mode, the command line API installation throws TypeError.
+ // The evaluation aborts early and returns the TypeError object.
+ InspectorTest.log('Evaluating with includeCommandLineAPI while paused in strict...');
+ const evalResponse = await Protocol.Runtime.evaluate({
+ expression: '2 + 2',
+ includeCommandLineAPI: true
+ });
+
+ // Log the full response to see the error.
+ InspectorTest.logMessage(evalResponse);
+
+ await Protocol.Debugger.resume();
+ },
+
+ async function testSloppyFrame() {
+ // Note: Global is already frozen from the previous test.
+ // runSloppy is also already defined.
+
+ // Run the sloppy function. It should pause.
+ const pausedPromise = Protocol.Debugger.oncePaused();
+ Protocol.Runtime.evaluate({ expression: 'runSloppy()' });
+ await pausedPromise;
+ InspectorTest.log('Paused in sloppy mode.');
+
+ // Evaluate with includeCommandLineAPI: true.
+ // In sloppy mode, the command line API installation fails silently.
+ // The evaluation proceeds and returns 4.
+ InspectorTest.log('Evaluating with includeCommandLineAPI while paused in sloppy...');
+ const evalResponse = await Protocol.Runtime.evaluate({
+ expression: '2 + 2',
+ includeCommandLineAPI: true
+ });
+
+ // Log the full response, should be value: 4.
+ InspectorTest.logMessage(evalResponse);
+
+ await Protocol.Debugger.resume();
+ }
+]);
Original Bug Report
DCHECK failure in !isolate->has_exception() in execution.cc
Detailed Report: https://clusterfuzz.com/testcase?key=4548820230766592
Fuzzer: ochang_js_fuzzer Job Type: linux_asan_d8_dbg Platform Id: linux
Crash Type: DCHECK failure Crash Address: Crash State: !isolate->has_exception() in execution.cc v8::internal::Invoke v8::internal::Execution::CallScript
Sanitizer: address (ASAN)
Regressed: https://clusterfuzz.com/revisions?job=linux_asan_d8_dbg&range=91856:91857
Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=4548820230766592
Issue filed automatically.
To reproduce this, please build the target in this report and run it against the reproducer testcase. Please use the GN arguments provided at bottom of this report when building the binary.
If you have trouble reproducing, please also export the environment variables listed under “[Environment]” in the crash stacktrace.
If you have any feedback on reproducing test cases, let us know at https://forms.gle/Yh3qCYFveHj6E5jz5 so we can improve.