Medium watch CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in V8
DescriptionOut of bounds write in V8
ComponentV8
Sub-componentDebug
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker502337304
Fix commit8be639c1053e (v8/v8) +60/-0
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-14 (per vendor advisory)
Fix landed2026-04-22 (public git)
Disclosed2026-06-02
Patch gapNot computed: mapped commit cites a different bug, not this CVE's bug.

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.setVariableValue protocol command.
`JavaScriptFrame::SetExpression(index, value)`
a method that writes value into slot index of 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.

Key insight
The single mistake was writing into a native-stack expression slot at a caller-influenced 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

  1. Pause a frame under the debugger Using the Inspector protocol, the attacker pauses execution in an unoptimized JavaScript frame (e.g. via a debugger statement) so a callFrameId scope is available.
  2. Invoke the variable-setting sink The attacker issues Debugger.setVariableValue (or the equivalent) which reaches ScopeIterator::SetLocalVariableValue, passing through the is_unoptimized() check to the SetExpression write.
  3. Supply an out-of-range index With a mismatched ScopeInfo/frame size the resolved index exceeds ComputeExpressionsCount(), so frame->SetExpression(index, *new_value) writes *new_value to an out-of-bounds offset on the native stack. The accompanying test notes that reaching a genuinely OOB index “requires a mismatched ScopeInfo and Frame size,” typically via bytecode corruption or a prior sandbox-relative primitive (inferred).
  4. 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

An attacker with the ability to drive the debugger/Inspector and produce a mismatched frame layout gains an out-of-bounds write to the native stack, which lies outside the V8 sandbox cage, in the renderer process. The advisory classifies this as an out-of-bounds write in V8; any progression to a full sandbox escape or code execution is (inferred) and not shown by the patch. A precondition is control over 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

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

Audit Directions

  • Frame slot writes
    Audit every caller of JavaScriptFrame::SetExpression and related frame register/slot setters for missing ComputeExpressionsCount()/bounds checks on caller-influenced indices.
  • Debugger-supplied indices
    Review 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 regressions
    Re-examine code touched by prior OOB fixes (here 502337304) for sibling write paths that the original patch left unguarded.
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();
+});
Loading diff…

Original Bug Report

reported by [email protected]

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
Uploaded PoC files