CVE-2026-11173
Overview
Background
- `ScopeIterator::SetLocalVariableValue`
- the V8 debugger routine (named in the patch subject) that writes a new value into a local variable of a paused frame, reached via the Inspector
Debugger.setVariableValueprotocol command. - `JavaScriptFrame::SetExpression(index, value)`
- a method that writes
valueinto slotindexof a frame’s expression/register area, which lives on the native (C++) machine stack. - `ComputeExpressionsCount()`
- returns the number of valid expression/register slots the current unoptimized frame actually has, i.e. the legitimate upper bound for
index. - V8 sandbox
- a mitigation that confines heap corruption to a virtual-memory cage; the native stack is outside that cage, so an OOB write to it bypasses the sandbox’s protection.
Root Cause Analysis
On the write path shown in the hunk, after GetFrame() returns a frame and the code confirms frame->is_unoptimized(), it called frame->SetExpression(index, *new_value) with no validation of index. Because index derives from attacker-influenced debugger input and was never checked against the frame’s actual slot count, a large or negative index caused SetExpression to compute an out-of-bounds slot address and write *new_value outside the frame’s expression area on the native stack. The violated invariant is that index must satisfy 0 <= index < ComputeExpressionsCount() before it can index into the frame’s expression slots.
The fix restores this invariant by inserting CHECK_GE(index, 0) and CHECK_LT(index, frame->ComputeExpressionsCount()) immediately before the SetExpression call, so an out-of-range index aborts the process rather than performing the write. The commit notes this is an incomplete-fix follow-up to bug 502337304, meaning an earlier attempt left this write path unguarded.
index without a bounds check; the fix adds explicit CHECK_GE/CHECK_LT bounds validation against ComputeExpressionsCount() so the write can only target a valid in-frame slot.Attack Path
- Pause a frame under the debugger
Using the Inspector protocol, the attacker pauses execution in an unoptimized JavaScript frame (e.g. via a
debuggerstatement) so acallFrameIdscope is available. - Invoke the variable-setting sink
The attacker issues
Debugger.setVariableValue(or the equivalent) which reachesScopeIterator::SetLocalVariableValue, passing through theis_unoptimized()check to theSetExpressionwrite. - Supply an out-of-range index
With a mismatched
ScopeInfo/frame size the resolvedindexexceedsComputeExpressionsCount(), soframe->SetExpression(index, *new_value)writes*new_valueto an out-of-bounds offset on the native stack. The accompanying test notes that reaching a genuinely OOBindex“requires a mismatched ScopeInfo and Frame size,” typically via bytecode corruption or a prior sandbox-relative primitive (inferred). - Escalate via controlled stack write A controlled out-of-bounds native-stack write outside the sandbox cage could be leveraged toward corrupting saved return addresses or other stack state (inferred); the patch itself only demonstrates the unguarded write.
Impact Assessment
index such that it falls outside [0, ComputeExpressionsCount()), which per the patch author normally requires bytecode corruption or an existing sandbox-relative capability.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
Audit Directions
- Frame slot writesAudit every caller of
JavaScriptFrame::SetExpressionand related frame register/slot setters for missingComputeExpressionsCount()/bounds checks on caller-influenced indices. - Debugger-supplied indicesReview other
ScopeIterator/Debugger.*mutators that map protocol input to slot or register indices, ensuring each validates range before indexing into stack- or frame-backed storage. - Incomplete-fix regressionsRe-examine code touched by prior OOB fixes (here
502337304) for sibling write paths that the original patch left unguarded.
Patch
From 8be639c1053e8756f297efdbda31320e06f54e11 Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
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 <bmeurer@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
Auto-Submit: Danil Somsikov <dsv@chromium.org>
Commit-Queue: Benedikt Meurer <bmeurer@chromium.org>
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.