CVE-2025-7656
Overview
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;
CheckVeneerPooldecides when to emit it based on a distance margin. - `count` argument
- the promised instruction count passed to the scope; multiplied by
kInstrSizeit tellsCheckVeneerPoolhow 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.
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
- 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 Functionwith 40000 parameters and repeated%OptimizeOsr(). - Reach the mis-sized scope
Code generation invokes the register push/pop or callee-saved save/restore helpers, which entered
InstructionAccurateScopewith the defaultcount == 0and thus reserved zero veneer margin. - Overflow the veneer distance margin
With the pool blocked but no margin reserved, the far-branch distance accumulates until the subsequent
CheckVeneerPooloverflows 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). - 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
(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
| Function | Change | Notes |
|---|---|---|
V8_NODISCARDsrc/codegen/arm64/macro-assembler-arm64.h |
modified | |
ifsrc/codegen/arm64/macro-assembler-arm64.h |
modified | |
fortest/mjsunit/regress/regress-425583995.js |
modified |
Files Changed
src/codegen/arm64/macro-assembler-arm64.ccsrc/codegen/arm64/macro-assembler-arm64.htest/mjsunit/regress/regress-425583995.js
Audit Directions
- Default-zero scope countsSearch other
InstructionAccurateScope(and analogous block-pool/const-pool scopes on arm64/arm) constructions that relied on the former defaultcount == 0and thus reserve no pool margin. - CFI-dependent instruction countsReview fixed instruction-count assumptions in prologue/epilogue and other hand-counted sequences that change under
V8_ENABLE_CONTROL_FLOW_INTEGRITYor pointer authentication. - Veneer/constant-pool margin arithmeticAudit
CheckVeneerPooland its distance/margin computations for int32 overflow when very large code objects are generated (e.g. functions with huge argument counts under Maglev/OSR).
Patch
From c58fda1f0ec46429dd66c2cacf6a98fac001e4fd Mon Sep 17 00:00:00 2001
From: Victor Gomes <victorgomes@chromium.org>
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 <victorgomes@chromium.org>
Commit-Queue: Victor Gomes <victorgomes@chromium.org>
Commit-Queue: Igor Sheludko <ishell@chromium.org>
Reviewed-by: Igor Sheludko <ishell@chromium.org>
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();
+}
Regression Test / PoC
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();
+}
Original Bug Report
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