High patch-now ✔︎ verified CVSS 8.8 chrome OOB 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in V8
DescriptionBuffer overflow in V8
ComponentV8
Sub-componentInterpreter
Bug ClassOOB
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: Mechanism matches commit message and diff. Minor: analysis says no severity is stated, but the advisory lists severity high. Test flags (--stack-size, --no-concurrent-sparkplug) are for determinism, not required preconditions.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker563716534
Fix commit4615af981a0b (v8/v8) +45/-0
CWECWE-121 (Stack buffer overflow)
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-09-19 (per vendor advisory)
Fix landed2026-09-25 (public git)
Disclosed2026-09-29
Patch gap4 days — fix public in source before the advisory

Background

Ignition vs. Sparkplug (baseline)
Ignition is V8’s bytecode interpreter; Sparkplug is the fast baseline JIT, and functions can transition (“OSR”, on-stack replacement) from the interpreter into baseline code while executing a loop.
`frame_size_` vs. `max_frame_size()`
BytecodeArray::frame_size_ is the interpreter frame’s local slots, while max_frame_size() is frame_size_ + max_arguments_, the extra room baseline code needs to push outgoing call arguments.
Entry stack checks
InterpreterEntryTrampoline checks only frame_size_ (Ignition’s call helpers stack-check before pushing args), whereas BaselineOutOfLinePrologue pre-checks max_frame_size() so baseline call sites can push arguments without a per-call stack check.
`real_jslimit` / `real_climit`
The true JS/C++ stack limits (kRealJsLimit) below which pushing further data overruns the stack.

Root Cause Analysis

When a function is entered in Ignition near the stack limit, InterpreterEntryTrampoline only reserved frame_size_, not max_frame_size(), because Ignition’s call helpers stack-check before pushing outgoing arguments. If that function then OSRs into Sparkplug from inside a loop, InterpreterOnStackReplacement_ToBaseline transfers into baseline code while bypassing BaselineOutOfLinePrologue, so the max_arguments_ headroom that baseline code assumes was never validated. Baseline call sites push outgoing arguments without their own stack check, so a subsequent call with many arguments can write past real_jslimit/real_climit, a stack buffer overflow.

The fix, in InterpreterAssembler::OnStackReplacement at the osr_to_sparkplug label, loads max_arguments_ from the BytecodeArray, converts it to bytes, and uses StackPointerGreaterThan(IntPtrAdd(stack_limit, max_arguments_bytes)) to verify enough remaining space before tail-calling the OSR builtin; if the space is insufficient it branches to skip_osr_to_sparkplug and executes JumpBackward(relative_jump), continuing the loop in Ignition instead of OSRing. Staying in the interpreter (rather than throwing StackOverflow at JumpLoop) is deliberate, since interpreter exception lookup is keyed by bytecode offset and an inner try-catch could otherwise mis-catch it.

Key insight
The mistake is that OSR from Ignition into Sparkplug inside a loop skipped the baseline prologue’s max_frame_size() (frame_size_ + max_arguments_) stack check, leaving baseline call sites free to push arguments past the real stack limit; the fix adds an explicit max_arguments_ headroom check before the OSR tail-call and falls back to continuing the loop in the interpreter when the space is not available.

Attack Path

  1. Enter near the stack limit Cause a function to be entered in Ignition when the stack is close to the limit, so only frame_size_ (not max_frame_size()) has been reserved; the PoC uses --stack-size=100 and a new Function body.
  2. OSR into Sparkplug from a loop Run a loop so a backedge interrupt triggers InterpreterOnStackReplacement_ToBaseline, moving execution into baseline code without the BaselineOutOfLinePrologue max_arguments_ check (for (let i = 0; i < 50; i++)).
  3. Issue a large-argument call From the now-baseline loop body, perform a call that pushes many outgoing arguments (String.fromCharCode(${many_char_codes}) with ~11000 args), which baseline pushes without a stack check.
  4. Overflow the stack The pushed arguments write past real_jslimit/real_climit, overrunning the stack (buffer overflow in V8). (inferred as to any further primitive)

Impact Assessment

An attacker running JavaScript in the renderer can drive a stack buffer overflow in V8 by forcing a near-limit Ignition-to-Sparkplug OSR followed by a many-argument call, writing outgoing arguments beyond the real stack limit. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8. Conversion of the stack overrun into a controlled write or renderer compromise is not demonstrated by the patch (inferred).

Changed Functions

FunctionChangeNotes
for
test/mjsunit/regress/regress-563716534.js
modified

Files Changed

  • test/mjsunit/regress/regress-563716534.js
  • src/interpreter/interpreter-assembler.cc

Audit Directions

  • Asymmetric prologue/entry invariants across tiers
    Audit every transition (OSR, tail-call, deopt) between execution tiers to confirm the destination tier’s stack/frame assumptions (max_frame_size(), argument headroom) are established when the normal prologue is bypassed.
  • Argument pushes without a stack check
    Look for call-emitting code that relies on a one-time entry/prologue check for outgoing-argument space, and verify no path reaches those pushes without that check having run.
  • Near-limit fallback behavior
    Review stack-limit handling that must avoid throwing at points where exception dispatch is offset-keyed (e.g. JumpLoop), ensuring fallbacks re-check on later interrupts rather than corrupting the stack.
From 4615af981a0b4775e01481d9fc3bfed02c5f8a68 Mon Sep 17 00:00:00 2001
From: Leszek Swirski <[email protected]>
Date: Fri, 25 Sep 2026 11:37:13 +0200
Subject: [PATCH] [interpreter] Check max_arguments stack space before Sparkplug OSR

InterpreterEntryTrampoline only checks BytecodeArray::frame_size_ on
function entry because Ignition call helpers check stack space before
pushing outgoing call arguments. Sparkplug, by contrast, relies on
BaselineOutOfLinePrologue pre-checking BytecodeArray::max_frame_size()
(frame_size_ + max_arguments_) at function entry and pushes outgoing
arguments at call sites without a stack check. When a function entered
in Ignition near the stack limit OSRs into Sparkplug inside a loop,
InterpreterOnStackReplacement_ToBaseline bypasses the baseline prologue,
allowing subsequent calls with many arguments to push past real_jslimit
and real_climit.

In InterpreterAssembler::OnStackReplacement, verify that there is enough
remaining stack space for max_arguments_ before tail-calling
InterpreterOnStackReplacement_ToBaseline, continuing the loop in
Ignition if not.

TAG=agy
CONV=8ac6e096-bff0-46cd-8db3-1f8046c78ca3

Fixed: 563716534
Change-Id: I4fca55df3627b1e36a7e57cc2c4a9655b2abbfcd
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8460873
Commit-Queue: Leszek Swirski <[email protected]>
Reviewed-by: Igor Sheludko <[email protected]>
Auto-Submit: Leszek Swirski <[email protected]>
Cr-Commit-Position: refs/heads/main@{#110091}
---
diff --git a/src/interpreter/interpreter-assembler.cc b/src/interpreter/interpreter-assembler.cc
index 074f827..b8fbf96 100644
--- a/src/interpreter/interpreter-assembler.cc
+++ b/src/interpreter/interpreter-assembler.cc
@@ -1599,12 +1599,40 @@
 
   BIND(&osr_to_sparkplug);
   {
+    Label skip_osr_to_sparkplug(this);
+    // InterpreterEntryTrampoline only checks BytecodeArray::frame_size_,
+    // whereas baseline code requires BytecodeArray::max_frame_size()
+    // (frame_size_ + max_arguments_) so it can push outgoing call arguments
+    // without a stack check. Since frame_size_ is already allocated in the
+    // current interpreter frame, verify that there is enough remaining stack
+    // space for max_arguments_ before OSRing into baseline code.
+    //
+    // If there isn't enough space, we skip OSR and stay in the interpreter
+    // rather than throwing a StackOverflow here: exception handler lookup in
+    // the interpreter is keyed by bytecode offset, so throwing at JumpLoop
+    // could be caught by an inner try-catch wrapping the loop (and the call
+    // requiring max_arguments_ might not even be reached). Skipping means we
+    // will re-check on subsequent backedge interrupts while near the stack
+    // limit, which is an acceptable cost for this edge case.
+    TNode<Uint16T> max_arguments = LoadObjectField<Uint16T>(
+        BytecodeArrayTaggedPointer(), offsetof(BytecodeArray, max_arguments_));
+    TNode<UintPtrT> max_arguments_bytes =
+        TimesSystemPointerSize(ChangeUint32ToWord(max_arguments));
+    TNode<UintPtrT> stack_limit =
+        Load<UintPtrT>(IsolateField(IsolateFieldId::kRealJsLimit));
+    GotoIfNot(
+        StackPointerGreaterThan(IntPtrAdd(stack_limit, max_arguments_bytes)),
+        &skip_osr_to_sparkplug);
+
     // We already compiled the baseline code, so we don't need to handle failed
     // compilation as in the Ignition -> Turbofan case. Therefore we can just
     // tailcall to the OSR builtin.
     SaveBytecodeOffset();
     TailCallBuiltin(Builtin::kInterpreterOnStackReplacement_ToBaseline,
                     context);
+
+    BIND(&skip_osr_to_sparkplug);
+    JumpBackward(relative_jump);
   }
 }
 
diff --git a/test/mjsunit/regress/regress-563716534.js b/test/mjsunit/regress/regress-563716534.js
new file mode 100644
index 0000000..45c80b2
--- /dev/null
+++ b/test/mjsunit/regress/regress-563716534.js
@@ -0,0 +1,17 @@
+// 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.
+
+// Flags: --no-concurrent-sparkplug --stack-size=100
+
+const many_char_codes = new Array(11000).fill(48).join(",");
+const loop_then_large_call = new Function(`
+  for (let i = 0; i < 50; i++) {
+    if (i === 49) return String.fromCharCode(${many_char_codes});
+  }
+`);
+
+try {
+  loop_then_large_call();
+} catch (e) {
+}
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/regress/regress-563716534.js b/test/mjsunit/regress/regress-563716534.js
new file mode 100644
index 0000000..45c80b2
--- /dev/null
+++ b/test/mjsunit/regress/regress-563716534.js
@@ -0,0 +1,17 @@
+// 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.
+
+// Flags: --no-concurrent-sparkplug --stack-size=100
+
+const many_char_codes = new Array(11000).fill(48).join(",");
+const loop_then_large_call = new Function(`
+  for (let i = 0; i < 50; i++) {
+    if (i === 49) return String.fromCharCode(${many_char_codes});
+  }
+`);
+
+try {
+  loop_then_large_call();
+} catch (e) {
+}
Loading diff…