Medium chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in V8
DescriptionInteger overflow in V8
ComponentV8
Bug ClassInteger Overflow
Tracker506629455
Fix commit19b8e4cb5e83 (v8/v8) +34/-41
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
src/baseline/baseline-compiler.cc
modified
if
src/codegen/arm/assembler-arm.cc
modified
if
src/codegen/arm64/assembler-arm64.cc
modified
if
src/codegen/assembler.cc
modified
V8_EXPORT_PRIVATE
src/codegen/assembler.h
modified
if
src/codegen/ia32/assembler-ia32.cc
modified

Files Changed

  • src/baseline/baseline-compiler.cc
  • src/codegen/arm/assembler-arm.cc
  • src/codegen/arm/assembler-arm.h
  • src/codegen/arm64/assembler-arm64.cc
  • src/codegen/arm64/assembler-arm64.h
  • src/codegen/assembler.cc
  • src/codegen/assembler.h
  • src/codegen/ia32/assembler-ia32.cc
  • src/codegen/ia32/assembler-ia32.h
From 19b8e4cb5e838e786d3883b531765b3b07514dfb Mon Sep 17 00:00:00 2001
From: Victor Gomes <victorgomes@chromium.org>
Date: Mon, 27 Apr 2026 15:32:54 +0200
Subject: [PATCH] [codegen] Fix signed integer overflow in Assembler::GrowBuffer

Compiling extremely large functions could cause `Assembler::GrowBuffer`
to calculate a negative buffer size due to a signed integer overflow
when doubling the size. This bypassed the out-of-memory guard and
resulted in a massive heap-based buffer overflow.

This CL fixes the issue by:
1. Moving `kMaximalBufferSize` to `AssemblerBase` as a common constant.
2. Adding a non-static helper method `ComputeNewBufferSize` to
   `AssemblerBase` that takes a `BufferGrowthStrategy` enum flag.
3. Adding a `DCHECK` to ensure the new size fits within `int` boundaries.
4. Updating `GrowBuffer` in arm, arm64, ia32, and x64 to use this helper.
5. Adding a check against `kMaximalBufferSize` in
   `BaselineCompiler::AllocateBuffer` to prevent huge initial
   allocations.

TAG=agy
CONV=d58d11be-b7cb-4eab-a315-ddefcb616292

Fixed: 506629455
Change-Id: Iff3ee491f911b9183c9680db84417de9e89ed72d
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7796004
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Auto-Submit: Victor Gomes <victorgomes@chromium.org>
Cr-Commit-Position: refs/heads/main@{#106882}
---

diff --git a/src/baseline/baseline-compiler.cc b/src/baseline/baseline-compiler.cc
index 9cbe073..7080fc6 100644
--- a/src/baseline/baseline-compiler.cc
+++ b/src/baseline/baseline-compiler.cc
@@ -276,7 +276,11 @@
     DisallowHeapAllocation no_gc;
     estimated_size = BaselineCompiler::EstimateInstructionSize(*bytecodes);
   }
-  return NewAssemblerBuffer(RoundUp(estimated_size, 4 * KB));
+  int rounded_size = RoundUp(estimated_size, 4 * KB);
+  if (rounded_size > Assembler::kMaximalBufferSize) {
+    V8::FatalProcessOutOfMemory(nullptr, "BaselineCompiler::AllocateBuffer");
+  }
+  return NewAssemblerBuffer(rounded_size);
 }
 }  // namespace
 
diff --git a/src/codegen/arm/assembler-arm.cc b/src/codegen/arm/assembler-arm.cc
index 0131469..47d62e7 100644
--- a/src/codegen/arm/assembler-arm.cc
+++ b/src/codegen/arm/assembler-arm.cc
@@ -5214,13 +5214,7 @@
 
   // Compute new buffer size.
   int old_size = buffer_->size();
-  int new_size = std::min(2 * old_size, old_size + 1 * MB);
-
-  // Some internal data structures overflow for very large buffers,
-  // they must ensure that kMaximalBufferSize is not too large.
-  if (new_size > kMaximalBufferSize) {
-    V8::FatalProcessOutOfMemory(nullptr, "Assembler::GrowBuffer");
-  }
+  int new_size = ComputeNewBufferSize(BufferGrowthStrategy::kDoubleCapped1MB);
 
   // Set up new buffer.
   std::unique_ptr<AssemblerBuffer> new_buffer = buffer_->Grow(new_size);
diff --git a/src/codegen/arm/assembler-arm.h b/src/codegen/arm/assembler-arm.h
index 9252748..64c4db2 100644
--- a/src/codegen/arm/assembler-arm.h
+++ b/src/codegen/arm/assembler-arm.h
@@ -1302,9 +1302,6 @@
   VfpRegList scratch_vfp_register_list_;
 
  private:
-  // Avoid overflows for displacements etc.
-  static const int kMaximalBufferSize = 512 * MB;
-
   // Constant pool generation
   // Pools are emitted in the instruction stream, preferably after unconditional
   // jumps or after returns from functions (in dead code locations).
diff --git a/src/codegen/arm64/assembler-arm64.cc b/src/codegen/arm64/assembler-arm64.cc
index d5a2f5a..a17aeb3 100644
--- a/src/codegen/arm64/assembler-arm64.cc
+++ b/src/codegen/arm64/assembler-arm64.cc
@@ -4681,13 +4681,7 @@
 void Assembler::GrowBuffer() {
   // Compute new buffer size.
   int old_size = buffer_->size();
-  int new_size = std::min(2 * old_size, old_size + 1 * MB);
-
-  // Some internal data structures overflow for very large buffers,
-  // they must ensure that kMaximalBufferSize is not too large.
-  if (new_size > kMaximalBufferSize) {
-    V8::FatalProcessOutOfMemory(nullptr, "Assembler::GrowBuffer");
-  }
+  int new_size = ComputeNewBufferSize(BufferGrowthStrategy::kDoubleCapped1MB);
 
   // Set up new buffer.
   std::unique_ptr<AssemblerBuffer> new_buffer = buffer_->Grow(new_size);
diff --git a/src/codegen/arm64/assembler-arm64.h b/src/codegen/arm64/assembler-arm64.h
index a642a62..3d007d6 100644
--- a/src/codegen/arm64/assembler-arm64.h
+++ b/src/codegen/arm64/assembler-arm64.h
@@ -3536,9 +3536,6 @@
 #endif
 
  private:
-  // Avoid overflows for displacements etc.
-  static const int kMaximalBufferSize = 512 * MB;
-
   // If a veneer is emitted for a branch instruction, that instruction must be
   // removed from the associated label's link chain so that the assembler does
   // not later attempt (likely unsuccessfully) to patch it to branch directly to
diff --git a/src/codegen/assembler.cc b/src/codegen/assembler.cc
index ea777c2..0065e4b 100644
--- a/src/codegen/assembler.cc
+++ b/src/codegen/assembler.cc
@@ -195,6 +195,25 @@
 
 AssemblerBase::~AssemblerBase() = default;
 
+int AssemblerBase::ComputeNewBufferSize(BufferGrowthStrategy strategy) {
+  int old_size = buffer_size();
+  int64_t new_size_64;
+
+  if (strategy == BufferGrowthStrategy::kDouble) {
+    new_size_64 = 2LL * old_size;
+  } else {
+    DCHECK_EQ(strategy, BufferGrowthStrategy::kDoubleCapped1MB);
+    new_size_64 = std::min<int64_t>(2LL * old_size,
+                                    static_cast<int64_t>(old_size) + 1 * MB);
+  }
+
+  if (new_size_64 > kMaximalBufferSize) {
+    V8::FatalProcessOutOfMemory(nullptr, "Assembler::GrowBuffer");
+  }
+  DCHECK_LE(new_size_64, kMaxInt);
+  return static_cast<int>(new_size_64);
+}
+
 void AssemblerBase::Print(Isolate* isolate) {
   StdoutStream os;
   v8::internal::Disassembler::Decode(isolate, os, buffer_start_, pc_);
diff --git a/src/codegen/assembler.h b/src/codegen/assembler.h
index ebd6ebb..c287995 100644
--- a/src/codegen/assembler.h
+++ b/src/codegen/assembler.h
@@ -332,6 +332,10 @@
 
 class V8_EXPORT_PRIVATE AssemblerBase : public Malloced {
  public:
+  static constexpr int kMaximalBufferSize = 512 * MB;
+
+  enum class BufferGrowthStrategy { kDouble, kDoubleCapped1MB };
+
   AssemblerBase(const AssemblerOptions& options,
                 std::unique_ptr<AssemblerBuffer>);
   virtual ~AssemblerBase();
@@ -410,6 +414,8 @@
     return buffer;
   }
 
+  int ComputeNewBufferSize(BufferGrowthStrategy strategy);
+
   // This function is called when code generation is aborted, so that
   // the assembler could clean up internal data structures.
   virtual void AbortedCodeGeneration() {}
diff --git a/src/codegen/ia32/assembler-ia32.cc b/src/codegen/ia32/assembler-ia32.cc
index 6b1cd01..4aa3706 100644
--- a/src/codegen/ia32/assembler-ia32.cc
+++ b/src/codegen/ia32/assembler-ia32.cc
@@ -3309,13 +3309,7 @@
 
   // Compute new buffer size.
   int old_size = buffer_->size();
-  int new_size = 2 * old_size;
-
-  // Some internal data structures overflow for very large buffers,
-  // they must ensure that kMaximalBufferSize is not too large.
-  if (new_size > kMaximalBufferSize) {
-    V8::FatalProcessOutOfMemory(nullptr, "Assembler::GrowBuffer");
-  }
+  int new_size = ComputeNewBufferSize(BufferGrowthStrategy::kDouble);
 
   // Set up new buffer.
   std::unique_ptr<AssemblerBuffer> new_buffer = buffer_->Grow(new_size);
diff --git a/src/codegen/ia32/assembler-ia32.h b/src/codegen/ia32/assembler-ia32.h
index b462cd8..bf89748 100644
--- a/src/codegen/ia32/assembler-ia32.h
+++ b/src/codegen/ia32/assembler-ia32.h
@@ -1686,9 +1686,6 @@
     return (buffer_start_ + buffer_->size()) - reloc_info_writer.pos();
   }
 
-  // Avoid overflows for displacements etc.
-  static constexpr int kMaximalBufferSize = 512 * MB;
-
   uint8_t byte_at(int pos) { return buffer_start_[pos]; }
Loading diff…

Original Bug Report

reported by rj...@google.com

Heap buffer overflow in V8 Sparkplug compiler via signed integer overflow in Assembler::GrowBuffer

Project Fortify has identified a security issue and generated a PoC.

d8 variant: ‘Default’

flags: –always-sparkplug –no-concurrent-sparkplug –omit-quit –fuzzing –disallow-unsafe-flags

Return code: 134

<details>

<summary>stdout</summary>

</details>

<details>

<summary>stderr</summary>



#
# Fatal error in ../../v8/src/base/macros.h, line 432
# Debug check failed: x >= 0 (-1790490720 vs. 0).
#
#
#
#FailureMessage Object: 0x7ffd97ea4180
==== C stack trace ===============================

    /home/rjlothian_google_com/chromium/src/out/Default/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x1e) [0x73132fb33c5e]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8_libplatform.so(+0x1660d) [0x73132fae260d]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x194) [0x73132fb15dc4]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8_libbase.so(+0x29675) [0x73132fb15675]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8.so(+0x24634fc) [0x731331fa24fc]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8.so(+0x32e0488) [0x731332e1f488]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8.so(+0x2211293) [0x731331d50293]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8.so(v8::internal::Compiler::CompileSharedWithBaseline(v8::internal::Isolate*, v8::internal::Handle<v8::internal::SharedFunctionInfo>, v8::internal::Compiler::ClearExceptionFlag, v8::internal::IsCompiledScope*)+0x20d) [0x731331e315dd]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8.so(+0x22f3114) [0x731331e32114]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8.so(v8::internal::Compiler::GetFunctionFromEval(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::String>, v8::internal::DirectHandle<v8::internal::SharedFunctionInfo>, v8::internal::DirectHandle<v8::internal::Context>, v8::internal::LanguageMode, v8::internal::ParseRestriction, int, int, v8::internal::ParsingWhileDebugging)+0x9a8) [0x731331e34ae8]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8.so(v8::internal::Compiler::GetFunctionFromValidatedString(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::NativeContext>, v8::internal::MaybeDirectHandle<v8::internal::String>, v8::internal::ParseRestriction, int)+0x9d) [0x731331e3616d]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8.so(+0x2271e06) [0x731331db0e06]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8.so(+0x226e65b) [0x731331dad65b]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8.so(+0x226e41d) [0x731331dad41d]
    /home/rjlothian_google_com/chromium/src/out/Default/libv8.so(+0x153bd3d) [0x73133107ad3d]
Received signal 6

</details>

Overview: V8’s Sparkplug baseline compiler is vulnerable to a massive heap-based buffer overflow outside the V8 sandbox due to a signed integer overflow. Compiling extremely large functions causes Assembler::GrowBuffer to calculate a negative buffer size, bypassing an out-of-memory check. This results in gigabytes of machine code being written into a 128-byte heap allocation, allowing arbitrary code execution via concurrent heap spraying.

Affected files:

  • v8/src/codegen/x64/assembler-x64.cc
  • v8/src/baseline/baseline-compiler.cc
  • v8/src/codegen/assembler.cc
  • v8/src/codegen/arm64/assembler-arm64.cc
  • v8/src/codegen/ia32/assembler-ia32.cc
  • v8/src/codegen/arm/assembler-arm.cc
  • v8/src/codegen/riscv/assembler-riscv.cc
  • v8/src/codegen/ppc/assembler-ppc.cc
  • v8/src/codegen/s390/assembler-s390.cc
  • v8/src/codegen/mips64/assembler-mips64.cc
  • v8/src/codegen/loong64/assembler-loong64.cc

Estimated timestamp from git blame: 2021-04-21

Root Cause

When V8’s Sparkplug compiler begins compiling a function, BaselineCompiler::AllocateBuffer estimates the required machine code buffer size. This is calculated as bytecode_length * 7 (on x64). For an exceptionally large BytecodeArray (e.g., ~154 MB), this initial allocation reaches ~1.07 GiB. Notably, this initial allocation lacks a check against kMaximalBufferSize (512 MiB).

If the generated machine code expands significantly (e.g., using bytecodes with large machine-code footprints), this 1.07 GiB buffer fills, triggering Assembler::GrowBuffer(). The root cause is a signed integer overflow when doubling the buffer size using 32-bit int arithmetic:

int old_size = buffer_->size(); // e.g., 1,078,984,704 (~1.07 GiB)
int new_size = 2 * old_size;    // Overflow! Wraps to a negative value (e.g., -2,136,997,888)

Because new_size wraps to a negative value, the subsequent out-of-memory guard is completely bypassed:

// A negative value is less than kMaximalBufferSize (512 MiB)
if (new_size > kMaximalBufferSize) {
  V8::FatalProcessOutOfMemory(nullptr, "Assembler::GrowBuffer");
}

The Overflow

The negative new_size is passed to buffer_->Grow(new_size). The allocation logic uses std::max(128, new_size), resulting in a new allocation of exactly 128 bytes on the C++ PartitionAlloc heap. MemMove then copies the original ~1.07 GiB of machine code into this 128-byte buffer, causing a massive out-of-bounds write.

Exploitation (V8 Sandbox Bypass)

The assembler buffer is allocated on the C++ heap, which resides outside the V8 sandbox. While the gigantic MemMove will eventually crash upon hitting an unmapped guard page, an attacker can exploit this via a concurrent Web Worker. By spraying the 128-byte bucket pool with target objects (like ArrayBuffer backing stores or function pointers), the worker can monitor the corruption and hijack control flow before the compiling thread reaches the guard page and segfaults.

Suggested Fix

  1. Use Checked Math: Change old_size and new_size in Assembler::GrowBuffer() to size_t, or use base::CheckedNumeric<int> to safely double the size and check for .IsValid() before proceeding.
  2. Validate Initial Allocation: Add an explicit guard in BaselineCompiler::AllocateBuffer to ensure the initially calculated estimated_size does not silently exceed Assembler::kMaximalBufferSize.

Evaluated with Chrome root at commit: 380c72ddde1aa946561f639736dcc63b52d92a6f


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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

View on issue tracker