CVE-2026-19168
Overview
Background
- Maglev
- V8’s mid-tier optimizing JIT compiler that generates machine code from bytecode faster than TurboFan but with fewer optimizations.
- `FunctionEntryStackCheck`
- A runtime check emitted in a generated function’s prologue that verifies the stack has room before the function proceeds.
- Guard page
- An unmapped OS memory page placed below the stack so that touching it faults, letting the OS detect stack overflow.
- Stack slot
- A fixed-size cell in a function’s stack frame reserved for a live value, counted here as
tagged_stack_slotsplusuntagged_stack_slots.
Root Cause Analysis
Maglev-generated code enlarges its stack frame with a single stack-pointer decrement in the prologue that executes before FunctionEntryStackCheck runs, so the entire frame must be small enough that this one decrement cannot skip past the OS guard page. Nothing bounded that frame size: a function that kept many Int32 and Float64 values live simultaneously could force Maglev to allocate hundreds or thousands of stack slots, and unlike TurboFan, Maglev did not honor max_optimized_bytecode_size, so arbitrarily large functions were also compiled. When the unchecked stack-pointer decrement exceeded a full page, it jumped clear over the guard page and landed on unrelated mapped memory, defeating the overflow-detection mechanism the guard page provides.
The fix makes MaglevCompiler::CheckGraph bail out when tagged_stack_slots() + untagged_stack_slots() exceeds kMaxStackSlots (one 4 KB page worth of pointers), and makes MaglevCompilationJob::PrepareJobImpl abort when the bytecode is larger than v8_flags.max_optimized_bytecode_size, so oversized frames simply never reach code generation.
Attack Path
- Craft a wide-frame function
Author JavaScript that keeps hundreds of
Int32andFloat64values live at once, forcing Maglev to reserve roughly two stack slots per value while the bytecode itself stays small. - Trigger Maglev optimization Run the function in a hot loop (or via OSR) so V8 promotes it to Maglev and generates code with a frame far larger than one page.
- Skip the guard page
On entry, the prologue’s single stack-pointer decrement jumps the stack pointer over the unmapped guard page before
FunctionEntryStackCheckexecutes. - Corrupt out-of-bounds stack memory Subsequent frame writes land on mapped memory below the intended stack region instead of faulting, silently corrupting adjacent data.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
fortest/mjsunit/maglev/regress-536945254.js |
modified |
Files Changed
src/codegen/compiler.ccsrc/maglev/maglev-compiler.ccsrc/maglev/maglev-concurrent-dispatcher.ccsrc/maglev/maglev-concurrent-dispatcher.htest/mjsunit/maglev/regress-536945254.js
Audit Directions
- Unchecked prologue stack adjustmentAudit every code generator that decrements the stack pointer before
FunctionEntryStackCheckand confirm the adjustment is bounded to at most one guard-page size. - Compiler tier limit parityVerify that all optimizing tiers (Maglev, TurboFan, and any future JIT) enforce the same
max_optimized_bytecode_sizeand stack-slot ceilings, since divergence reintroduces this class of bug. - `CHECK` on recoverable compilation statusSearch for
CHECK/CHECK_EQon compilation-job status that assume success, and replace them with graceful bailouts so newly added failure paths do not turn into crashes.
Patch
From d6a92ca4858786aef57a8cf49b359742ea1194aa Mon Sep 17 00:00:00 2001 From: Victor Gomes <[email protected]> Date: Fri, 24 Jul 2026 14:13:13 +0200 Subject: [PATCH] [maglev] Bound the stack frame size of generated code Maglev grows its frame with a single stack pointer decrement, before FunctionEntryStackCheck runs, so frames have to stay within the size. Maglev now bails out on max_optimized_bytecode_size like TurboFan. It also bails out if the number of stack slots is bigger than an OS page. Fixed: 536945254 Change-Id: I7355d2257bebcf62b756a7f57c327d4f27677685 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8137738 Auto-Submit: Victor Gomes <[email protected]> Reviewed-by: Darius Mercadier <[email protected]> Commit-Queue: Darius Mercadier <[email protected]> Cr-Commit-Position: refs/heads/main@{#108869} --- diff --git a/src/codegen/compiler.cc b/src/codegen/compiler.cc index 70a3fa0..af54b3e 100644 --- a/src/codegen/compiler.cc +++ b/src/codegen/compiler.cc @@ -1220,7 +1220,7 @@ CompilerTracer::TraceStartMaglevCompile(isolate, function, job->is_osr(), mode); CompilationJob::Status status = job->PrepareJob(isolate); - CHECK_EQ(status, CompilationJob::SUCCEEDED); // TODO(v8:7700): Use status. + if (status != CompilationJob::SUCCEEDED) return {}; } if (IsSynchronous(mode)) { diff --git a/src/maglev/maglev-compiler.cc b/src/maglev/maglev-compiler.cc index c2fcebc..050da4b 100644 --- a/src/maglev/maglev-compiler.cc +++ b/src/maglev/maglev-compiler.cc @@ -53,6 +53,8 @@ namespace { +constexpr uint32_t kMaxStackSlots = 4 * KB / kSystemPointerSize; + void PrintGraph(Graph* graph, bool condition, MaglevPhase phase) { MaglevCompilationInfo* info = graph->compilation_info(); if (V8_UNLIKELY(condition && info->is_tracing_enabled())) { @@ -279,6 +281,15 @@ } } + // The prologue grows the frame with a single stack pointer decrement, before + // FunctionEntryStackCheck runs. A frame no larger than the smallest OS page + // cannot step over a guard page, so bail out rather than emit an unchecked + // stack pointer jump of arbitrary size. + if (graph->tagged_stack_slots() + graph->untagged_stack_slots() > + kMaxStackSlots) { + return false; + } + { TRACE_EVENT(TRACE_DISABLED_BY_DEFAULT("v8.compile"), "V8.Maglev.CodeAssembly"); diff --git a/src/maglev/maglev-concurrent-dispatcher.cc b/src/maglev/maglev-concurrent-dispatcher.cc index ed62dde..9c1da9c 100644 --- a/src/maglev/maglev-concurrent-dispatcher.cc +++ b/src/maglev/maglev-concurrent-dispatcher.cc @@ -113,7 +113,20 @@ MaglevCompilationJob::~MaglevCompilationJob() = default; +CompilationJob::Status MaglevCompilationJob::AbortOptimization( + Isolate* isolate, BailoutReason reason) { + DCHECK_EQ(ThreadId::Current(), isolate->thread_id()); + bailout_reason_ = reason; + DirectHandle<SharedFunctionInfo> shared(function()->shared(), isolate); + shared->DisableOptimization(isolate, reason); + return UpdateState(FAILED, State::kFailed); +} + CompilationJob::Status MaglevCompilationJob::PrepareJobImpl(Isolate* isolate) { + if (function()->shared()->GetBytecodeArray(isolate)->length() > + v8_flags.max_optimized_bytecode_size) { + return AbortOptimization(isolate, BailoutReason::kFunctionTooBig); + } BeginPhaseKind("V8.MaglevPrepareJob"); if (info()->collect_source_positions()) { SharedFunctionInfo::EnsureSourcePositionsAvailable( diff --git a/src/maglev/maglev-concurrent-dispatcher.h b/src/maglev/maglev-concurrent-dispatcher.h index 5f33973..aa71cea 100644 --- a/src/maglev/maglev-concurrent-dispatcher.h +++ b/src/maglev/maglev-concurrent-dispatcher.h @@ -35,6 +35,8 @@ LocalIsolate* local_isolate) override; Status FinalizeJobImpl(Isolate* isolate) override; + Status AbortOptimization(Isolate* isolate, BailoutReason reason); + IndirectHandle<JSFunction> function() const; MaybeIndirectHandle<Code> code() const; BytecodeOffset osr_offset() const; diff --git a/test/mjsunit/maglev/regress-536945254.js b/test/mjsunit/maglev/regress-536945254.js new file mode 100644 index 0000000..522bcfd --- /dev/null +++ b/test/mjsunit/maglev/regress-536945254.js @@ -0,0 +1,62 @@ +// 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: --allow-natives-syntax --maglev --no-turbofan + +// Keeps `count` Int32 and Float64 values live at once, so Maglev needs roughly +// two stack slots per value while the bytecode stays small. +function buildWideFrame(count, osr) { + let preload = ''; + let integerFold = ''; + let doubleFold = ''; + for (let i = 0; i < count; ++i) { + preload += `0.5 + (input ^ ${i});`; + integerFold += `integer ^= (input ^ ${i});`; + doubleFold += `double += (input ^ ${i});`; + } + return Function(` + return function wide(input) { + input |= 0; + let result = 0; + for (let iteration = 0; iteration < 20; ++iteration) { + ${osr ? 'if (iteration === 5) %OptimizeOsr();' : ''} + ${preload} + let integer = 0; + ${integerFold} + let double = 0.5; + ${doubleFold} + result = (integer + double) | 0; + } + return result; + }; + `)(); +} + +// ~750 stack slots, ~16KB of bytecode. +const kWideValueCount = 400; + +const wide = buildWideFrame(kWideValueCount, false); +%PrepareFunctionForOptimization(wide); +const expected = wide(1); +%OptimizeMaglevOnNextCall(wide); +assertEquals(expected, wide(1)); +assertFalse(isOptimized(wide)); + +const wideOsr = buildWideFrame(kWideValueCount, true); +%PrepareFunctionForOptimization(wideOsr); +assertEquals(expected, wideOsr(1)); + +// One live value, but more bytecode than Maglev is willing to compile. +function buildLongBytecode(count) { + let body = ''; + for (let i = 0; i < count; ++i) body += `sum += ${i};`; + return Function(`return function long(sum) { ${body} return sum; };`)(); +} + +const long = buildLongBytecode(12000); +%PrepareFunctionForOptimization(long); +const expectedLong = long(0); +%OptimizeMaglevOnNextCall(long); +assertEquals(expectedLong, long(0)); +assertFalse(isOptimized(long));
Regression Test / PoC
diff --git a/test/mjsunit/maglev/regress-536945254.js b/test/mjsunit/maglev/regress-536945254.js
new file mode 100644
index 0000000..522bcfd
--- /dev/null
+++ b/test/mjsunit/maglev/regress-536945254.js
@@ -0,0 +1,62 @@
+// 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: --allow-natives-syntax --maglev --no-turbofan
+
+// Keeps `count` Int32 and Float64 values live at once, so Maglev needs roughly
+// two stack slots per value while the bytecode stays small.
+function buildWideFrame(count, osr) {
+ let preload = '';
+ let integerFold = '';
+ let doubleFold = '';
+ for (let i = 0; i < count; ++i) {
+ preload += `0.5 + (input ^ ${i});`;
+ integerFold += `integer ^= (input ^ ${i});`;
+ doubleFold += `double += (input ^ ${i});`;
+ }
+ return Function(`
+ return function wide(input) {
+ input |= 0;
+ let result = 0;
+ for (let iteration = 0; iteration < 20; ++iteration) {
+ ${osr ? 'if (iteration === 5) %OptimizeOsr();' : ''}
+ ${preload}
+ let integer = 0;
+ ${integerFold}
+ let double = 0.5;
+ ${doubleFold}
+ result = (integer + double) | 0;
+ }
+ return result;
+ };
+ `)();
+}
+
+// ~750 stack slots, ~16KB of bytecode.
+const kWideValueCount = 400;
+
+const wide = buildWideFrame(kWideValueCount, false);
+%PrepareFunctionForOptimization(wide);
+const expected = wide(1);
+%OptimizeMaglevOnNextCall(wide);
+assertEquals(expected, wide(1));
+assertFalse(isOptimized(wide));
+
+const wideOsr = buildWideFrame(kWideValueCount, true);
+%PrepareFunctionForOptimization(wideOsr);
+assertEquals(expected, wideOsr(1));
+
+// One live value, but more bytecode than Maglev is willing to compile.
+function buildLongBytecode(count) {
+ let body = '';
+ for (let i = 0; i < count; ++i) body += `sum += ${i};`;
+ return Function(`return function long(sum) { ${body} return sum; };`)();
+}
+
+const long = buildLongBytecode(12000);
+%PrepareFunctionForOptimization(long);
+const expectedLong = long(0);
+%OptimizeMaglevOnNextCall(long);
+assertEquals(expectedLong, long(0));
+assertFalse(isOptimized(long));
Original Bug Report
Unprobed Maglev OSR frame growth can skip stack guards
VULNERABILITY DETAILS
M150 Maglev code generation can reserve a very large OSR frame with one stack-pointer subtraction without probing the intervening pages before using the frame. This implementation defect is not confined to Linux or x86-64: the same unprobed adjustment for the remaining untagged slots appears in the M150 x64, arm64, arm32, RISC-V, PPC, S390, and LoongArch64 backends.
The attached PoC was validated only on Linux x86-64. On that configuration, a sufficiently large untagged spill area jumps over a pthread guard page into an adjacent mapped thread stack, and later spills overwrite the other renderer thread’s live native frame. On other targets, the consequence depends on the operating system’s stack reservation, guard, and mapping layout; this report does not claim that the attached PoC works unchanged or that every target yields adjacent-stack corruption.
The following is the x64 implementation exercised by the validated exploit. Tagged slots are pushed and therefore touched, but the remaining untagged slots are allocated as follows:
uint32_t size_so_far = source_frame_size + additional_tagged;
if (size_so_far < target_frame_size) {
subq(rsp,
Immediate((target_frame_size - size_so_far) * kSystemPointerSize));
}
There is no page probing or guard-aware preflight around this subtraction. The generated FunctionEntryStackCheck executes after the prologue has already built the frame, so it cannot prevent the guard-page jump.
The attached page demonstrates the resulting cross-thread write:
- It creates two equal-sized renderer pthread stacks in adjacent reusable holes. A lower-address worker runs a Liftoff function with 10,000 live
i64locals; the higher-address worker is the attacker. - The attacker generates a Maglev OSR body with 500,084 expressions whose integer and floating-point forms remain live, producing a very large raw spill area.
- A recursive Proxy trap measures the current stack limit and positions the attacker close to its guard page.
- OSR entry performs the unprobed subtraction. The new RSP lands beyond the guard, in the victim’s mapped stack, and Maglev spills change the victim’s live Wasm locals.
The page contains no native payload. It stops at cross-thread corruption (or the resulting renderer failure).
The bug is reachable from ordinary web content in stock Chrome without a prior memory-corruption primitive or non-default browser flags. COOP/COEP response headers are used only to enable SharedArrayBuffer coordination and can be supplied by an attacker-controlled origin. The resulting overwrite targets a live native renderer stack and can corrupt saved control-flow state, making this a renderer-code-execution primitive; the attached minimal PoC intentionally stops at cross-thread data corruption.
VERSION
- Validated Chrome configuration: Stable
150.0.7871.128, Linux x86-64 - Code-level scope: the unprobed OSR adjustment is present in every M150 Maglev architecture backend listed above; exploitability outside Linux x86-64 was not tested
- Chromium revision:
81891e5ca708047763816c778216799ef14c66cb - V8 revision:
2b2f69158528fdd9d86b778cfcc2d0a1c4f8c59f - Chrome GNU build ID:
2f071daf2a0b4aa68bda1729031294d443a989ab - OS used for validation: Ubuntu 24.04 x86-64
REPRODUCTION CASE
Attachments:
poc.html— self-contained trigger, including both worker bodies and generated Wasm.server.py— Python standard-library server adding the COOP/COEP headers required forSharedArrayBuffer.
Reproduction steps:
-
Put both files in one directory.
-
Start the server:
cd stackclash python3 server.py -
Start a fresh Stable M150 process and navigate to the PoC:
google-chrome-stable \ --user-data-dir=/tmp/chrome-maglev-stack-poc \ http://127.0.0.1:8000/poc.html -
The page reports setup phases. On the tested machine, the target renderer failed about 18 seconds after navigation, shortly after
RUNNING initialize attacker. Depending on stack reuse and the overwritten words, another run may instead reportCONTEXT_OVERLAP_VICTIM_CHANGEDwith the changed live-local index and value.
The defaults in poc.html are the qualified M150 geometry. Query parameters are present only to make minimization and retesting easier; none are required.
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION
Type of crash: renderer/tab process.
Crash State: One M150 focused run produced:
exception: SIGTRAP (signal 5)
faulting thread: 0x316496
RIP: chrome+0x314e32b
instruction at chrome+0x314e32a: int3 (followed by ud2)
captured stack: [0x7488085d8160, 0x7488085e5000)
Chrome build ID: 2f071daf2a0b4aa68bda1729031294d443a989ab
An ASan diagnostic is not expected for the defining write: after RSP skips the guard, the destination is a valid, mapped neighboring pthread stack. The victim’s changed live locals provide direct evidence of the cross-thread write.
CREDIT INFORMATION
Reporter credit: Found by XBOW and triaged by Andrés Luksenberg
- http://127.0.0.1:8000/poc.html
- https://chromium.googlesource.com/v8/v8/+/2b2f69158528fdd9d86b778cfcc2d0a1c4f8c59f/src/maglev/arm/maglev-assembler-arm.cc#100
- https://chromium.googlesource.com/v8/v8/+/2b2f69158528fdd9d86b778cfcc2d0a1c4f8c59f/src/maglev/arm64/maglev-assembler-arm64.cc#100
- https://chromium.googlesource.com/v8/v8/+/2b2f69158528fdd9d86b778cfcc2d0a1c4f8c59f/src/maglev/loong64/maglev-assembler-loong64.cc#110
- https://chromium.googlesource.com/v8/v8/+/2b2f69158528fdd9d86b778cfcc2d0a1c4f8c59f/src/maglev/maglev-ir.cc#1199
- https://chromium.googlesource.com/v8/v8/+/2b2f69158528fdd9d86b778cfcc2d0a1c4f8c59f/src/maglev/ppc/maglev-assembler-ppc.cc#101
- https://chromium.googlesource.com/v8/v8/+/2b2f69158528fdd9d86b778cfcc2d0a1c4f8c59f/src/maglev/riscv/maglev-assembler-riscv.cc#105
- https://chromium.googlesource.com/v8/v8/+/2b2f69158528fdd9d86b778cfcc2d0a1c4f8c59f/src/maglev/s390/maglev-assembler-s390.cc#101
- https://chromium.googlesource.com/v8/v8/+/2b2f69158528fdd9d86b778cfcc2d0a1c4f8c59f/src/maglev/x64/maglev-assembler-x64.cc#516