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 <[email protected]> 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 <[email protected]> Commit-Queue: Nico Hartmann <[email protected]> Auto-Submit: Igor Sheludko <[email protected]> Reviewed-by: Nico Hartmann <[email protected]> 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();