CVE-2026-11173
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
iftest/inspector/debugger/set-variable-value-oob.js |
modified |
Files Changed
src/debug/debug-scopes.cctest/inspector/debugger/set-variable-value-oob-expected.txttest/inspector/debugger/set-variable-value-oob.js
Patch
From 8be639c1053e8756f297efdbda31320e06f54e11 Mon Sep 17 00:00:00 2001 From: Danil Somsikov <[email protected]> Date: Tue, 21 Apr 2026 06:55:19 -0700 Subject: [PATCH] [debug] Fix OOB stack write in ScopeIterator::SetLocalVariableValue This CL mitigates an out-of-bounds (OOB) stack write vulnerability by adding explicit bounds checks to `ScopeIterator::SetLocalVariableValue` when setting a local variable. Prior to this fix, a missing bounds check allowed an out-of-bounds write to the native stack when a high `index` was passed. Since the V8 sandbox protects the heap, this vulnerability could be exploited to write outside the sandbox cage by targeting the native stack via `SetExpression`. This CL adds `CHECK_GE` and `CHECK_LT` to validate the `index` against `frame->ComputeExpressionsCount()` before performing the write. This addresses an incomplete fix from a previous issue (502337304). Bug: 503649569 Change-Id: Ibf98696697d5b3268db8c218c3e88dfa4626ef70 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7780454 Reviewed-by: Benedikt Meurer <[email protected]> Reviewed-by: Simon Zünd <[email protected]> Auto-Submit: Danil Somsikov <[email protected]> Commit-Queue: Benedikt Meurer <[email protected]> Cr-Commit-Position: refs/heads/main@{#106689} --- diff --git a/src/debug/debug-scopes.cc b/src/debug/debug-scopes.cc index c032173..ea3149d 100644 --- a/src/debug/debug-scopes.cc +++ b/src/debug/debug-scopes.cc @@ -1137,6 +1137,8 @@ JavaScriptFrame* frame = GetFrame(); if (!frame->is_unoptimized()) return false; + CHECK_GE(index, 0); + CHECK_LT(index, frame->ComputeExpressionsCount()); frame->SetExpression(index, *new_value); } return true; diff --git a/test/inspector/debugger/set-variable-value-oob-expected.txt b/test/inspector/debugger/set-variable-value-oob-expected.txt new file mode 100644 index 0000000..0735486 --- /dev/null +++ b/test/inspector/debugger/set-variable-value-oob-expected.txt @@ -0,0 +1,5 @@ +Tests mitigation of OOB stack write in SetExpression +Paused in trigger() +Successfully set variable a to 0x1337 +Value of a: 4919 +trigger() returned: 4919 diff --git a/test/inspector/debugger/set-variable-value-oob.js b/test/inspector/debugger/set-variable-value-oob.js new file mode 100644 index 0000000..8a40413 --- /dev/null +++ b/test/inspector/debugger/set-variable-value-oob.js @@ -0,0 +1,53 @@ +// 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 { contextGroup, Protocol } = InspectorTest.start( + 'Tests mitigation of OOB stack write in SetExpression' +); + +contextGroup.addScript(` +function trigger() { + let a = 1; + debugger; + return a; +} +`); + +Protocol.Debugger.enable(); +Protocol.Debugger.onPaused(async message => { + const callFrameId = message.params.callFrames[0].callFrameId; + + InspectorTest.log('Paused in trigger()'); + + // Attempt to write a valid local variable to ensure the new CHECK bounds do not regress functionality. + // Note: Triggering an actual OOB index requires a mismatched ScopeInfo and Frame size, which + // usually requires bytecode corruption or a sandbox escape mechanism. This test ensures + // the added mitigation does not crash valid use cases. + const response = await Protocol.Debugger.setVariableValue({ + scopeNumber: 0, + variableName: 'a', + newValue: { value: 0x1337 }, + callFrameId + }); + + if (response.error) { + InspectorTest.log('Error setting variable: ' + response.error.message); + } else { + InspectorTest.log('Successfully set variable a to 0x1337'); + } + + const evalResponse = await Protocol.Debugger.evaluateOnCallFrame({ + callFrameId, + expression: 'a' + }); + + InspectorTest.log('Value of a: ' + evalResponse.result.result.value); + + await Protocol.Debugger.resume(); +}); + +Protocol.Runtime.evaluate({ expression: 'trigger()' }).then(response => { + InspectorTest.log('trigger() returned: ' + response.result.result.value); + InspectorTest.completeTest(); +});
Original Bug Report
V8 Sandbox Bypass via OOB Stack Write in ScopeIterator::SetLocalVariableValue
Project Fortify has identified a security issue and generated a PoC.
d8 variant: ‘Asan’
flags: –enable-inspector –sandbox-testing –expose-memory-corruption-api –fuzzing
Return code: 139
<details>
<summary>stdout</summary>
</details>
<details>
<summary>stderr</summary>
Sandbox testing mode is enabled. Only sandbox violations will be reported, all other crashes will be ignored.
Sandbox bounds: [0x70ce00000000,0x71ce00000000)
## V8 sandbox violation detected!
Received signal 11 SEGV_MAPERR 7ffdc862c748
==== C stack trace ===============================
bin/Asan/d8(___interceptor_backtrace+0x46)[0x595cdb5c4d16]
bin/Asan/d8(+0x739e929)[0x595ce13b4929]
bin/Asan/d8(+0x3247563)[0x595cdd25d563]
/lib/x86_64-linux-gnu/libc.so.6(+0x45330)[0x761687c45330]
bin/Asan/d8(+0x1dcf2e7)[0x595cdbde52e7]
bin/Asan/d8(+0x1cce5a6)[0x595cdbce45a6]
bin/Asan/d8(+0x1ccd847)[0x595cdbce3847]
bin/Asan/d8(+0x4961b4b)[0x595cde977b4b]
bin/Asan/d8(+0x489f3f3)[0x595cde8b53f3]
bin/Asan/d8(+0x4a3d721)[0x595cdea53721]
bin/Asan/d8(+0x49d2c00)[0x595cde9e8c00]
bin/Asan/d8(+0x16da5c6)[0x595cdb6f05c6]
bin/Asan/d8(+0x70f8664)[0x595ce110e664]
[end of stack trace]
Segmentation fault (core dumped)
</details>
Overview: The V8 debugger’s variable update mechanism is vulnerable to an out-of-bounds write on the native C++ stack. An attacker with arbitrary read/write within the V8 sandbox can corrupt a script’s source code, causing the debugger to reparse it with an inflated number of variables. When a variable is updated via the debugger, V8 writes to the native stack using the unvalidated index, allowing control flow hijacking and sandbox escape.
Affected files:
v8/src/debug/debug-scopes.ccv8/src/execution/frames.ccv8/src/execution/frames-inl.h
Estimated timestamp from git blame: 2026-02-13
Root Cause
When a local variable or parameter is modified via the Chrome DevTools Protocol (CDP) command Debugger.setVariableValue, V8 invokes ScopeIterator::SetLocalVariableValue. To locate the variable, the debugger reparses the function’s source code, which is retrieved from Script::source().
Under the V8 sandbox threat model, the Script object and its source string reside inside the sandbox and are attacker-controllable. An attacker with in-sandbox read/write access can corrupt the source code of a paused function to include an excessive number of parameters or local variables. When the debugger reparses this corrupted source, it assigns a massive index to the newly injected variables.
ScopeIterator::SetLocalVariableValue then uses this untrusted index to update the variable on the stack:
- For parameters, it calls
JavaScriptFrame::SetParameterValue(index, ...), which writes tocaller_sp() + (index + 1) * 8. - For locals, it calls
frame->SetExpression(index, ...), which writes tofp() + offset - index * 8.
Crucially, the underlying offset calculations (CommonFrameWithJSLinkage::GetParameterSlot and CommonFrame::GetExpressionAddress) only validate the index using DCHECKs. In release builds, there are no bounds checks. This allows the attacker to specify an arbitrary index and perform an out-of-bounds write of a 64-bit V8 object pointer directly to the native C++ stack.
Impact
An attacker can overwrite saved return addresses or native C++ pointers on the stack, bypassing stack canaries and achieving native Remote Code Execution (RCE) outside the V8 sandbox. Since this requires an active debugger session (e.g., DevTools open), it relies on user interaction, but it represents a full bypass of the V8 sandbox’s security guarantees.
Suggested Fix
Introduce explicit release-build bounds checks when calculating stack slot addresses or writing to the stack.
For example, in CommonFrameWithJSLinkage::GetParameterSlot and CommonFrame::GetExpressionAddress, replace the DCHECKs with actual bounds checks (e.g., CHECK) that validate the index against the actual executing frame’s parameter or expression count. Alternatively, enforce bounds checking within ScopeIterator::SetLocalVariableValue before modifying the frame.
Evaluated with Chrome root at commit: bdfb9c25e02c7ac58c2db7565d32b3044ee487a6
The description of the vuln is LLM-generated and can contain mistakes. Your feedback is appreciated, and will help us make improvement over time. The PoC was run in a VM and it seemed to be legit - if not, let us know and we can strengthen our checker.