Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in V8
DescriptionOut of bounds write in V8
ComponentV8
Bug ClassOOB
Tracker502337304
Fix commit8be639c1053e (v8/v8) +60/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
test/inspector/debugger/set-variable-value-oob.js
modified

Files Changed

  • src/debug/debug-scopes.cc
  • test/inspector/debugger/set-variable-value-oob-expected.txt
  • test/inspector/debugger/set-variable-value-oob.js
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();
+});
Loading diff…

Original Bug Report

reported by rj...@google.com

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.cc
  • v8/src/execution/frames.cc
  • v8/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 to caller_sp() + (index + 1) * 8.
  • For locals, it calls frame->SetExpression(index, ...), which writes to fp() + 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.

View on issue tracker