CVE-2026-10987
Overview
Background
- Baseline / Sparkplug compiler
- V8’s fast, non-optimizing baseline tier that generates machine code straight from bytecode;
BaselineCompiler::AllocateBuffersizes theAssemblerBufferit emits into. - `EstimateInstructionSize`
- Estimates output code size as
bytecode->length() * kAverageBytecodeToInstructionRatio, used both to size the assembler buffer and to accumulate a batch-compilation budget. - `Assembler::kMaximalBufferSize`
- The upper bound on assembler buffer size;
AllocateBufferRoundUps the estimate to 4 KB and bails toFatalProcessOutOfMemoryif it exceeds this cap. - `base::CheckedNumeric<int>`
- A wrapper that tracks overflow through arithmetic and exposes it via
AssignIfValid/ValueOrDefault, instead of silently wrapping like a rawint.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Unchecked int multiply is confirmed. The diff shows no undersized buffer being overrun; the sink is only a fatal OOM abort, so the corruption primitive is unsupported. The batch-compiler overflow only affects a compilation budget counter.
EstimateInstructionSize computed bytecode->length() * kAverageBytecodeToInstructionRatio in plain int, so for a sufficiently large BytecodeArray the multiplication overflowed and could produce a small or negative value. In AllocateBuffer that value was passed to RoundUp(estimated_size, 4 * KB) and then checked against Assembler::kMaximalBufferSize; an overflowed-small estimated_size passes the rounded_size > kMaximalBufferSize guard, so the code allocates an AssemblerBuffer far too small for the actual code the baseline compiler will emit. Baseline code generation then writes past the undersized buffer, a heap buffer overflow (inferred as the ultimate sink; the diff shows the size miscalculation and the now-added cap, not the write).
The fix makes EstimateInstructionSize return base::CheckedNumeric<int>, and AllocateBuffer now rejects the estimate with FatalProcessOutOfMemory when AssignIfValid fails (overflow) or when the raw value already exceeds kMaximalBufferSize, before any RoundUp. The batch compiler path is likewise hardened: estimated_instruction_size_ += estimated_size is replaced with CheckedNumeric accumulation clamped via ValueOrDefault(kMaxInt), so the running budget cannot overflow either.
int multiplication, letting a huge bytecode array overflow the estimate to a small/negative value that slipped past the kMaximalBufferSize cap and produced an undersized assembler buffer; the fix routes the estimate through base::CheckedNumeric<int> and fatally aborts on overflow before allocation.Attack Path
- Build a huge bytecode array
Get JavaScript compiled to a
BytecodeArraywhoselength()is large enough thatlength() * kAverageBytecodeToInstructionRatiooverflows a 32-bitint. - Trigger baseline compilation
Warm the function so Sparkplug/baseline compiles it, calling
AllocateBuffer→EstimateInstructionSize. - Overflow the estimate past the cap
The unchecked multiply wraps to a small/negative
estimated_size, soRoundUpand therounded_size > kMaximalBufferSizecheck are satisfied and an undersizedAssemblerBufferis allocated. - Overflow the buffer during codegen
Baseline code generation emits the real (large) amount of machine code into the too-small buffer, writing out of bounds on the heap (
inferred; the diff adds the overflow detection rather than showing the write).
Impact Assessment
(inferred). The precondition is only attacker JavaScript large enough to overflow the int estimate plus normal baseline-tier warm-up. The advisory classifies this as an integer overflow in V8 and does not state exploited-in-the-wild.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/baseline/baseline-batch-compiler.cc |
modified | |
ifsrc/baseline/baseline-compiler.cc |
modified |
Files Changed
src/baseline/baseline-batch-compiler.ccsrc/baseline/baseline-compiler.ccsrc/baseline/baseline-compiler.h
Audit Directions
- Size/length estimates in `int`Audit code-size, buffer-size, and capacity estimates derived from
BytecodeArray/object lengths times a constant; any rawintmultiply feeding an allocation should usebase::CheckedNumeric. - Guards defeated by prior overflowLook for
> kMaximalBufferSize/ cap checks that run after an unchecked arithmetic step, where a wrapped small/negative value passes the check. - Accumulated budgetsReview running counters like
estimated_instruction_size_that sum per-item estimates over a batch; ensure summation is overflow-checked and clamped.
Patch
From ebf970d8a36498ea34eb9bf9fdfe8574c6bd8cf3 Mon Sep 17 00:00:00 2001
From: Leszek Swirski <leszeks@chromium.org>
Date: Fri, 22 May 2026 14:46:48 +0200
Subject: [PATCH] [baseline] Fix integer overflow in instruction size estimation
Use base::CheckedNumeric<int> for EstimateInstructionSize to prevent
integer overflow when calculating estimated instruction size for very
large bytecode arrays.
Also update baseline batch compiler to handle CheckedNumeric and prevent
overflow of estimated_instruction_size_.
Bug: 515431687
Change-Id: Id2208f5d95d81bd99adcde656ebcabcee9f38cce
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7866066
Auto-Submit: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Patrick Thier <pthier@chromium.org>
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Patrick Thier <pthier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#107527}
---
diff --git a/src/baseline/baseline-batch-compiler.cc b/src/baseline/baseline-batch-compiler.cc
index ca0dbc9..11c2f01 100644
--- a/src/baseline/baseline-batch-compiler.cc
+++ b/src/baseline/baseline-batch-compiler.cc
@@ -7,6 +7,7 @@
#include <algorithm>
#include "src/base/fpu.h"
+#include "src/base/numerics/checked_math.h"
#include "src/baseline/baseline-compiler.h"
#include "src/codegen/compiler.h"
#include "src/execution/isolate.h"
@@ -342,20 +343,23 @@
if (shared->is_sparkplug_compiling()) return false;
if (!CanCompileWithBaseline(isolate_, shared)) return false;
- int estimated_size;
+ base::CheckedNumeric<int> estimated_size;
{
DisallowHeapAllocation no_gc;
estimated_size = BaselineCompiler::EstimateInstructionSize(
shared->GetBytecodeArray(isolate_));
}
- estimated_instruction_size_ += estimated_size;
+ int raw_estimated_size = estimated_size.ValueOrDefault(kMaxInt);
+ base::CheckedNumeric<int> new_size = estimated_instruction_size_;
+ new_size += raw_estimated_size;
+ estimated_instruction_size_ = new_size.ValueOrDefault(kMaxInt);
if (v8_flags.trace_baseline_batch_compilation) {
CodeTracer::Scope trace_scope(isolate_->GetCodeTracer());
PrintF(trace_scope.file(), "[Baseline batch compilation] Enqueued SFI %s",
shared->DebugNameCStr().get());
PrintF(trace_scope.file(),
- " with estimated size %d (current budget: %d/%d)\n", estimated_size,
- estimated_instruction_size_,
+ " with estimated size %d (current budget: %d/%d)\n",
+ raw_estimated_size, estimated_instruction_size_,
v8_flags.baseline_batch_compilation_threshold.value());
}
if (estimated_instruction_size_ >=
diff --git a/src/baseline/baseline-compiler.cc b/src/baseline/baseline-compiler.cc
index bdf8087..7ba2329 100644
--- a/src/baseline/baseline-compiler.cc
+++ b/src/baseline/baseline-compiler.cc
@@ -271,12 +271,17 @@
#endif
std::unique_ptr<AssemblerBuffer> AllocateBuffer(
DirectHandle<BytecodeArray> bytecodes) {
- int estimated_size;
+ base::CheckedNumeric<int> estimated_size;
{
DisallowHeapAllocation no_gc;
estimated_size = BaselineCompiler::EstimateInstructionSize(*bytecodes);
}
- int rounded_size = RoundUp(estimated_size, 4 * KB);
+ int raw_estimated_size;
+ if (!estimated_size.AssignIfValid(&raw_estimated_size) ||
+ raw_estimated_size > Assembler::kMaximalBufferSize) {
+ V8::FatalProcessOutOfMemory(nullptr, "BaselineCompiler::AllocateBuffer");
+ }
+ int rounded_size = RoundUp(raw_estimated_size, 4 * KB);
if (rounded_size > Assembler::kMaximalBufferSize) {
V8::FatalProcessOutOfMemory(nullptr, "BaselineCompiler::AllocateBuffer");
}
@@ -367,8 +372,10 @@
return code_builder.TryBuild();
}
-int BaselineCompiler::EstimateInstructionSize(Tagged<BytecodeArray> bytecode) {
- return bytecode->length() * kAverageBytecodeToInstructionRatio;
+base::CheckedNumeric<int> BaselineCompiler::EstimateInstructionSize(
+ Tagged<BytecodeArray> bytecode) {
+ return base::CheckedNumeric<int>(bytecode->length()) *
+ kAverageBytecodeToInstructionRatio;
}
interpreter::Register BaselineCompiler::RegisterOperand(int operand_index) {
diff --git a/src/baseline/baseline-compiler.h b/src/baseline/baseline-compiler.h
index 14b3ead..23df4ef 100644
--- a/src/baseline/baseline-compiler.h
+++ b/src/baseline/baseline-compiler.h
@@ -6,6 +6,7 @@
#define V8_BASELINE_BASELINE_COMPILER_H_
#include "src/base/logging.h"
+#include "src/base/numerics/checked_math.h"
#include "src/base/pointer-with-payload.h"
#include "src/base/threaded-list.h"
#include "src/base/vlq.h"
@@ -55,7 +56,8 @@
void GenerateCode();
MaybeHandle<Code> Build();
- static int EstimateInstructionSize(Tagged<BytecodeArray> bytecode);
+ static base::CheckedNumeric<int> EstimateInstructionSize(
+ Tagged<BytecodeArray> bytecode);
private:
void Prologue();
Original Bug Report
Integer overflow in BaselineCompiler instruction size estimation
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential signed integer overflow in V8’s Sparkplug (baseline) compiler during instruction size estimation could allow very large functions to bypass an initial size check. This leads to the allocation of a minimal buffer that subsequently grows until it hits a strictly enforced 512MB limit, resulting in a controlled process termination.
Affected files:
v8/src/codegen/x64/assembler-x64.ccv8/src/baseline/baseline-compiler.ccv8/src/codegen/assembler.ccv8/src/codegen/arm64/assembler-arm64.ccv8/src/baseline/baseline.ccv8/src/codegen/compiler.ccv8/src/execution/tiering-manager.cc
Estimated timestamp from git blame: 2021-04-21
Potential Vulnerability Summary
A potential signed integer overflow exists in the V8 baseline compiler’s initial buffer allocation logic. For functions with extremely large bytecode arrays, the estimated machine code size can overflow a 32-bit signed integer, bypassing a capacity check and leading to an inefficient initial allocation. However, current safeguards in the assembler’s buffer growth logic prevent this from escalating into a memory corruption vulnerability.
Technical Analysis
When V8’s Sparkplug compiler prepares to compile a function, it estimates the required instruction buffer size using BaselineCompiler::EstimateInstructionSize. On most architectures, this estimate is calculated by multiplying the BytecodeArray length by 7.
For functions with very large bytecode arrays (approximately 306.8 MiB or larger), the result of this multiplication can overflow a 32-bit signed integer, wrapping around to a negative value. In baseline::AllocateBuffer, this overflowed value is checked against Assembler::kMaximalBufferSize (512 MiB). Because the value is negative, the check is bypassed, and a call to NewAssemblerBuffer is made.
Inside the buffer allocation logic, the negative size is processed by std::max(kMinimalBufferSize, size), resulting in the allocation of a minimal 128-byte buffer. As the compiler begins to emit instructions into this buffer, it quickly triggers a call to Assembler::GrowBuffer.
In current V8 versions, architecture-specific GrowBuffer implementations utilize the ComputeNewBufferSize helper. This helper employs 64-bit integer arithmetic (int64_t) for size calculations and performs a strict check against the 512 MiB kMaximalBufferSize before any reallocation occurs. If the compiler attempts to emit more code than the limit allows, V8 triggers a controlled FatalProcessOutOfMemory (OOM) crash.
Potential Attack Steps
- A potential attacker defines a JavaScript function containing a massive number of bytecodes (e.g., millions of simple logical operations) designed to produce a
BytecodeArrayexceeding 306.8 MiB. - The attacker triggers Sparkplug compilation of this function (e.g., by repeatedly calling it until it tiers up).
- The estimation logic overflows, bypassing the initial 512 MiB check and initializing the compiler with a 128-byte buffer.
- As the compiler attempts to grow the buffer incrementally to accommodate the JIT code, it eventually reaches the 512 MiB cap.
Impact and Fix Recommendation
The current impact is limited to a Denial-of-Service (DoS) via a process crash when extremely large functions are encountered. The reported sandbox bypass and linear heap overflow are mitigated by the robust size checks in ComputeNewBufferSize and the 512 MiB limit on the code buffer size.
To resolve the underlying logic flaw, we recommend using size_t or base::CheckedNumeric for all instruction size estimations and buffer allocation checks in v8/src/baseline/baseline-compiler.cc to prevent integer overflows during the estimation phase.
Note: These are potential steps based on code analysis; our current tooling does not allow for a live proof-of-concept execution.
Evaluated with Chrome root at commit: 29093e11cf509e3593f6229e4b1b075cca356049
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.