CVE-2026-14407
Overview
Background
- AAPCS64
- the arm64 procedure-call standard, which requires a callee to preserve only the lower 64 bits (the
d8–d15view) of the SIMD/FP registersv8–v15, leaving their upper 64 bits and all other V registers caller-saved. - D vs V registers
- on arm64 a
dregister is the lower 64 bits of the corresponding 128-bitv(Q/SIMD) register, so saving adregister saves only half of the full SIMD value (kDRegSizeInBitsvskQRegSizeInBits). - `CPURegList::GetCalleeSavedV` / `GetCallerSavedV`
- helpers that build the register lists V8 uses to decide which FP/SIMD registers to save and restore around calls.
- `SaveFPRegsMode::kSave`
- the mode in which V8’s macro-assembler spills FP registers to the stack around a runtime/C++ call.
Root Cause Analysis
The pre-patch code treated the full 128-bit v8–v15 as callee-saved: kNumberOfCalleeSavedVRegisters was 8 and GetCalleeSavedV(size) defaulted to describing those registers, while GetCallerSavedV(size) and the macro-assembler’s kStackSavedSavedFPSizeInBits used a size that, in non-Wasm builds, saved only the lower 64 bits (kDRegSizeInBits) of the caller-saved FP registers. This violates AAPCS64, which only guarantees the lower 64 bits of v8–v15 survive a call — the upper 64 bits are volatile — so generated V8 code that relied on the full 128-bit SIMD contents of those registers being preserved across a C++/runtime call could observe them silently corrupted by the callee.
The fix splits the lists into distinct D and V variants, sets kNumberOfCalleeSavedVRegisters to 0 (no 128-bit V register is callee-saved), makes GetCallerSavedV() cover all v0–v31 at kQRegSizeInBits, and replaces the size constant with a kSaveFullFPRegistersOnStack boolean so that when V8_ENABLE_WEBASSEMBLY is set the assembler unconditionally saves the full V registers (GetCallerSavedV()) rather than just the D halves. This restores the invariant that any SIMD state V8 needs across a call is actually spilled/restored.
v8–v15 as fully callee-saved when AAPCS64 only preserves their lower 64 bits, so the upper halves of SIMD registers could be clobbered by a C++ callee; the fix reclassifies all 128-bit V registers as caller-saved and unconditionally saves the full V registers on Wasm builds.Attack Path
- Populate SIMD state
Execute WebAssembly (or other code) that places live 128-bit SIMD values in
v8–v15, whose upper halves the old logic assumed the callee would preserve. - Cross a C++ boundary
Trigger a path that calls into C++/runtime code with
SaveFPRegsMode::kSave, where the pre-patch assembler saved only the lower 64 bits (kDRegSizeInBits) of the FP registers. - Clobber the upper halves
The C++ callee, permitted by AAPCS64 to use the upper 64 bits of
v8–v15, overwrites them; on return the SIMD register holds corrupted data because only the D half was restored. - Consume corrupted SIMD value Generated code resumes using the now-corrupted 128-bit register, producing incorrect SIMD computation results, matching the advisory’s “Inappropriate implementation in V8”. (inferred)
Impact Assessment
LogicError / “Inappropriate implementation in V8” and does not report in-the-wild exploitation. The precondition is V8_ENABLE_WEBASSEMBLY builds on arm64 exercising SIMD registers across a saving call boundary; any escalation from computation corruption to a memory-safety primitive is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/codegen/arm64/macro-assembler-arm64.cc |
modified |
Files Changed
src/codegen/arm64/assembler-arm64.ccsrc/codegen/arm64/constants-arm64.hsrc/codegen/arm64/macro-assembler-arm64.ccsrc/codegen/arm64/reglist-arm64.hsrc/compiler/backend/arm64/code-generator-arm64.cc
Audit Directions
- ABI register-preservation assumptionsReview other architectures/backends for lists that mark SIMD/vector registers as callee-saved when the platform ABI only preserves a sub-width, especially where full-width values live across calls.
- Partial-width save/restoreSearch for FP/SIMD spill code that saves a narrower view (D vs Q, xmm vs ymm/zmm) than the register width actually in use across a call boundary.
- Wasm-gated save pathsAudit
#if V8_ENABLE_WEBASSEMBLYconditionals that change how much register state is preserved, ensuring non-Wasm assumptions do not leak SIMD state corruption into paths that still use wide registers.
Patch
From a769cf1ffbec4beb7d8a3f8a6e72a425cb08f3cc Mon Sep 17 00:00:00 2001
From: Igor Sheludko <ishell@chromium.org>
Date: Wed, 27 May 2026 11:11:29 +0200
Subject: [PATCH] [arm64] Split Callee/CallerSaved FP register lists into D and V lists
... representing double and quad (128-bit SIMD) FP registers.
This CL makes sure that SIMD registers are not corrupted by a call to
C++ code by unconditionally saving/restoring them when Wasm support is
enabled.
TAG=agy
Fixed: 513586956
Change-Id: I45d656a5851db72ca20bb98c0b9d25d4c00f4d36
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7876383
Commit-Queue: Igor Sheludko <ishell@chromium.org>
Commit-Queue: Nico Hartmann <nicohartmann@chromium.org>
Auto-Submit: Igor Sheludko <ishell@chromium.org>
Reviewed-by: Nico Hartmann <nicohartmann@chromium.org>
Cr-Commit-Position: refs/heads/main@{#107584}
---
diff --git a/src/codegen/arm64/assembler-arm64.cc b/src/codegen/arm64/assembler-arm64.cc
index bbfb671..f3309d3 100644
--- a/src/codegen/arm64/assembler-arm64.cc
+++ b/src/codegen/arm64/assembler-arm64.cc
@@ -232,28 +232,41 @@
DCHECK_EQ(Count() % 2, 0);
}
-CPURegList CPURegList::GetCalleeSaved(int size) {
- return CPURegList(CPURegister::kRegister, size, 19, 28);
+CPURegList CPURegList::GetCalleeSaved() {
+ return CPURegList(CPURegister::kRegister, kXRegSizeInBits, 19, 28);
}
-CPURegList CPURegList::GetCalleeSavedV(int size) {
- return CPURegList(CPURegister::kVRegister, size, 8, 15);
+CPURegList CPURegList::GetCalleeSavedD() {
+ // AAPCS64 only requires the callee to preserve d8-d15 which are the
+ // *lower* 64 bits of v8-v15.
+ return CPURegList(CPURegister::kVRegister, kDRegSizeInBits, 8, 15);
}
-CPURegList CPURegList::GetCallerSaved(int size) {
+CPURegList CPURegList::GetCalleeSavedV() {
+ return CPURegList(kQRegSizeInBits, Simd128RegList{});
+}
+
+CPURegList CPURegList::GetCallerSaved() {
// x18 is the platform register and is reserved for the use of platform ABIs.
// Registers x0-x17 are caller-saved.
- CPURegList list = CPURegList(CPURegister::kRegister, size, 0, 17);
+ CPURegList list = CPURegList(CPURegister::kRegister, kXRegSizeInBits, 0, 17);
return list;
}
-CPURegList CPURegList::GetCallerSavedV(int size) {
+CPURegList CPURegList::GetCallerSavedD() {
// Registers d0-d7 and d16-d31 are caller-saved.
- CPURegList list = CPURegList(CPURegister::kVRegister, size, 0, 7);
- list.Combine(CPURegList(CPURegister::kVRegister, size, 16, 31));
+ CPURegList list = CPURegList(CPURegister::kVRegister, kDRegSizeInBits, 0, 7);
+ list.Combine(CPURegList(CPURegister::kVRegister, kDRegSizeInBits, 16, 31));
return list;
}
+CPURegList CPURegList::GetCallerSavedV() {
+ // AAPCS64 only requires the callee to preserve the *lower* 64 bits of v8-v15
+ // (which are essentially the d8-d15 registers), thus all V registers are
+ // caller-saved.
+ return CPURegList(CPURegister::kVRegister, kQRegSizeInBits, 0, 31);
+}
+
// -----------------------------------------------------------------------------
// Implementation of RelocInfo
diff --git a/src/codegen/arm64/constants-arm64.h b/src/codegen/arm64/constants-arm64.h
index ed9531d..6417831 100644
--- a/src/codegen/arm64/constants-arm64.h
+++ b/src/codegen/arm64/constants-arm64.h
@@ -44,7 +44,11 @@
// Callee saved registers are x19-x28.
constexpr int kNumberOfCalleeSavedRegisters = 10;
// Callee saved FP registers are d8-d15.
-constexpr int kNumberOfCalleeSavedVRegisters = 8;
+constexpr int kNumberOfCalleeSavedDRegisters = 8;
+// AAPCS64 only requires the callee to preserve the *lower* 64 bits of v8-v15
+// (which are essentially the d8-d15 registers), thus none of the 128-bit
+// V registers are callee-saved.
+constexpr int kNumberOfCalleeSavedVRegisters = 0;
constexpr int kWRegSizeInBits = 32;
constexpr int kWRegSizeInBitsLog2 = 5;
constexpr int kWRegSize = kWRegSizeInBits >> 3;
diff --git a/src/codegen/arm64/macro-assembler-arm64.cc b/src/codegen/arm64/macro-assembler-arm64.cc
index 398aa35..9d4ae75 100644
--- a/src/codegen/arm64/macro-assembler-arm64.cc
+++ b/src/codegen/arm64/macro-assembler-arm64.cc
@@ -49,9 +49,9 @@
// For WebAssembly we care about the full floating point register. If we are not
// running Wasm, we can get away with saving half of those registers.
#if V8_ENABLE_WEBASSEMBLY
-constexpr int kStackSavedSavedFPSizeInBits = kQRegSizeInBits;
+constexpr bool kSaveFullFPRegistersOnStack = true;
#else
-constexpr int kStackSavedSavedFPSizeInBits = kDRegSizeInBits;
+constexpr bool kSaveFullFPRegistersOnStack = false;
#endif // V8_ENABLE_WEBASSEMBLY
} // namespace
@@ -147,7 +147,11 @@
int bytes = list.TotalSizeInBytes();
if (fp_mode == SaveFPRegsMode::kSave) {
- auto fp_list = CPURegList::GetCallerSavedV(kStackSavedSavedFPSizeInBits);
+ // TODO(all): consider splitting SaveFPRegsMode::kSave into kSaveFPOnly
+ // and kSaveFPAndSIMD. The former is useful for those functions that don't
+ // use SIMD registers.
+ auto fp_list = kSaveFullFPRegistersOnStack ? CPURegList::GetCallerSavedV()
+ : CPURegList::GetCallerSavedD();
DCHECK_EQ(fp_list.Count() % 2, 0);
bytes += fp_list.TotalSizeInBytes();
}
@@ -166,7 +170,11 @@
int bytes = list.TotalSizeInBytes();
if (fp_mode == SaveFPRegsMode::kSave) {
- auto fp_list = CPURegList::GetCallerSavedV(kStackSavedSavedFPSizeInBits);
+ // TODO(all): consider splitting SaveFPRegsMode::kSave into kSaveFPOnly
+ // and kSaveFPAndSIMD. The former is useful for those functions that don't
+ // use SIMD registers.
+ auto fp_list = kSaveFullFPRegistersOnStack ? CPURegList::GetCallerSavedV()
+ : CPURegList::GetCallerSavedD();
DCHECK_EQ(fp_list.Count() % 2, 0);
PushCPURegList(fp_list);
bytes += fp_list.TotalSizeInBytes();
@@ -178,7 +186,11 @@
ASM_CODE_COMMENT(this);
int bytes = 0;
if (fp_mode == SaveFPRegsMode::kSave) {
- auto fp_list = CPURegList::GetCallerSavedV(kStackSavedSavedFPSizeInBits);
+ // TODO(all): consider splitting SaveFPRegsMode::kSave into kSaveFPOnly
+ // and kSaveFPAndSIMD. The former is useful for those functions that don't
+ // use SIMD registers.
+ auto fp_list = kSaveFullFPRegistersOnStack ? CPURegList::GetCallerSavedV()
+ : CPURegList::GetCallerSavedD();
DCHECK_EQ(fp_list.Count() % 2, 0);
PopCPURegList(fp_list);
bytes += fp_list.TotalSizeInBytes();
diff --git a/src/codegen/arm64/reglist-arm64.h b/src/codegen/arm64/reglist-arm64.h
index 64a3fb2..9af1f83 100644
--- a/src/codegen/arm64/reglist-arm64.h
+++ b/src/codegen/arm64/reglist-arm64.h
@@ -99,14 +99,19 @@
CPURegister PopHighestIndex();
// AAPCS64 callee-saved registers.
- static CPURegList GetCalleeSaved(int size = kXRegSizeInBits);
- static CPURegList GetCalleeSavedV(int size = kDRegSizeInBits);
+ static CPURegList GetCalleeSaved();
+ // Note that D registers are the lower 64-bit parts of respective V
+ // registers.
+ static CPURegList GetCalleeSavedD();
+ static CPURegList GetCalleeSavedV();
// AAPCS64 caller-saved registers. Note that this includes lr.
- // TODO(all): Determine how we handle d8-d15 being callee-saved, but the top
- // 64-bits being caller-saved.
- static CPURegList GetCallerSaved(int size = kXRegSizeInBits);
- static CPURegList GetCallerSavedV(int size = kDRegSizeInBits);
+ static CPURegList GetCallerSaved();
+ // Note that D registers are the lower 64-bit parts of respective V
+ // registers. Thus there's no need to save both D and V registers, saving
+ // just V registers is enough.
+ static CPURegList GetCallerSavedD();
+ static CPURegList GetCallerSavedV();
bool IsEmpty() const { return list_ == 0; }
@@ -163,10 +168,12 @@
// AAPCS64 callee-saved registers.
#define kCalleeSaved CPURegList::GetCalleeSaved()
+#define kCalleeSavedD CPURegList::GetCalleeSavedD()
#define kCalleeSavedV CPURegList::GetCalleeSavedV()
// AAPCS64 caller-saved registers. Note that this includes lr.
#define kCallerSaved CPURegList::GetCallerSaved()
+#define kCallerSavedD CPURegList::GetCallerSavedD()
#define kCallerSavedV CPURegList::GetCallerSavedV()
} // namespace internal
diff --git a/src/compiler/backend/arm64/code-generator-arm64.cc b/src/compiler/backend/arm64/code-generator-arm64.cc
index 3354b5e..4c750a6 100644
--- a/src/compiler/backend/arm64/code-generator-arm64.cc
+++ b/src/compiler/backend/arm64/code-generator-arm64.cc
@@ -4255,7 +4255,9 @@
CPURegList(kDRegSizeInBits, call_descriptor->CalleeSavedFPRegisters());
int saved_count = saves_fp.Count();