Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Sub-componentARM64
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker513586956
Fix commita769cf1ffbec (v8/v8) +95/-39
CWECWE-94, CWE-119 (Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-15 (per vendor advisory)
Fix landed2026-05-27 (public git)
Disclosed2026-06-30
Patch gap34 days — fix public in source before the advisory

Background

AAPCS64
the arm64 procedure-call standard, which requires a callee to preserve only the lower 64 bits (the d8–d15 view) of the SIMD/FP registers v8–v15, leaving their upper 64 bits and all other V registers caller-saved.
D vs V registers
on arm64 a d register is the lower 64 bits of the corresponding 128-bit v (Q/SIMD) register, so saving a d register saves only half of the full SIMD value (kDRegSizeInBits vs kQRegSizeInBits).
`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.

Key insight
The core mistake was modeling 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

  1. 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.
  2. 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.
  3. 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.
  4. 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

An attacker running crafted WebAssembly SIMD code in the renderer’s V8 engine could have live 128-bit register values silently corrupted across calls into C++, leading to incorrect/undefined SIMD results within the renderer process; the advisory classifies this as a 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

FunctionChangeNotes
if
src/codegen/arm64/macro-assembler-arm64.cc
modified

Files Changed

  • src/codegen/arm64/assembler-arm64.cc
  • src/codegen/arm64/constants-arm64.h
  • src/codegen/arm64/macro-assembler-arm64.cc
  • src/codegen/arm64/reglist-arm64.h
  • src/compiler/backend/arm64/code-generator-arm64.cc

Audit Directions

  • ABI register-preservation assumptions
    Review 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/restore
    Search 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 paths
    Audit #if V8_ENABLE_WEBASSEMBLY conditionals 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.
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();
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.