CVE-2026-5873
Overview
Files Changed
src/compiler/backend/arm64/instruction-selector-arm64.cctest/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc
Patch
From 4ef5cc27aa50b4a7e3096bbbffaf5058a811a2a9 Mon Sep 17 00:00:00 2001
From: Matthias Liedtke <mliedtke@chromium.org>
Date: Fri, 27 Mar 2026 11:53:00 +0100
Subject: [PATCH] [compiler][arm64] Reenable implicit truncation and force explicit zero-extension of load/store index
Bug: 496301615
Change-Id: Ibb905e90bee08ce83ce77bd7400e716e47d61882
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7705594
Commit-Queue: Matthias Liedtke <mliedtke@chromium.org>
Reviewed-by: Darius Mercadier <dmercadier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#106107}
---
diff --git a/src/compiler/backend/arm64/instruction-selector-arm64.cc b/src/compiler/backend/arm64/instruction-selector-arm64.cc
index 3cb256a..1f8bc26 100644
--- a/src/compiler/backend/arm64/instruction-selector-arm64.cc
+++ b/src/compiler/backend/arm64/instruction-selector-arm64.cc
@@ -581,13 +581,10 @@
OpIndex index, InstructionOperand* index_op,
InstructionOperand* shift_immediate_op) {
if (!selector->CanCover(node, index)) return false;
- if (const ChangeOp* change =
- selector->Get(index).TryCast<Opmask::kChangeUint32ToUint64>();
- change && selector->CanCover(index, change->input())) {
- index = change->input();
- }
const ShiftOp* shift = selector->Get(index).TryCast<Opmask::kShiftLeft>();
- if (shift == nullptr) return false;
+ if (shift == nullptr || shift->rep != RegisterRepresentation::WordPtr()) {
+ return false;
+ }
if (!g->CanBeLoadStoreShiftImmediate(shift->right(), rep)) return false;
*index_op = g->UseRegister(shift->left());
*shift_immediate_op = g->UseImmediate(shift->right());
@@ -3126,8 +3123,9 @@
void InstructionSelector::VisitTruncateInt64ToInt32(OpIndex node) {
Arm64OperandGenerator g(this);
- OpIndex value = Cast<ChangeOp>(node).input();
- Emit(kArm64Mov32, g.DefineAsRegister(node), g.UseRegister(value));
+ // The top 32 bits in the 64-bit register will be undefined, and
+ // must not be used by a dependent node.
+ EmitIdentity(node);
}
void InstructionSelector::VisitFloat64Mod(OpIndex node) {
diff --git a/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc b/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc
index d5dbdbd..f9460c4 100644
--- a/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc
+++ b/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc
@@ -278,7 +278,7 @@
{{TSUnop::kChangeUint32ToUint64, "ChangeUint32ToUint64", kArm64Mov32,
MachineType::Uint64()},
MachineType::Uint32()},
- {{TSUnop::kTruncateWord64ToWord32, "TruncateWord64ToWord32", kArm64Mov32,
+ {{TSUnop::kTruncateWord64ToWord32, "TruncateWord64ToWord32", kArchNop,
MachineType::Int32()},
MachineType::Int64()},
{{TSUnop::kChangeInt32ToFloat64, "ChangeInt32ToFloat64",
@@ -2571,17 +2571,12 @@
m.Word64ShiftRightArithmetic(p, m.Int32Constant(32)));
m.Return(t);
Stream s = m.Build();
- ASSERT_EQ(2U, s.size());
+ ASSERT_EQ(1U, s.size());
EXPECT_EQ(kArm64Asr, s[0]->arch_opcode());
ASSERT_EQ(2U, s[0]->InputCount());
EXPECT_EQ(s.ToVreg(p), s.ToVreg(s[0]->InputAt(0)));
EXPECT_EQ(32, s.ToInt64(s[0]->InputAt(1)));
ASSERT_EQ(1U, s[0]->OutputCount());
-
- EXPECT_EQ(kArm64Mov32, s[1]->arch_opcode());
- ASSERT_EQ(1U, s[1]->InputCount());
- EXPECT_EQ(s.ToVreg(s[0]->Output()), s.ToVreg(s[1]->InputAt(0)));
- ASSERT_EQ(1U, s[1]->OutputCount());
}
TEST_F(TurboshaftInstructionSelectorTest,
@@ -2593,17 +2588,12 @@
m.Word64ShiftRightLogical(p, m.Int32Constant(x)));
m.Return(t);
Stream s = m.Build();
- ASSERT_EQ(2U, s.size());
+ ASSERT_EQ(1U, s.size());
EXPECT_EQ(kArm64Lsr, s[0]->arch_opcode());
ASSERT_EQ(2U, s[0]->InputCount());
EXPECT_EQ(s.ToVreg(p), s.ToVreg(s[0]->InputAt(0)));
EXPECT_EQ(x, s.ToInt64(s[0]->InputAt(1)));
ASSERT_EQ(1U, s[0]->OutputCount());
-
- EXPECT_EQ(kArm64Mov32, s[1]->arch_opcode());
- ASSERT_EQ(1U, s[1]->InputCount());
- EXPECT_EQ(s.ToVreg(s[0]->Output()), s.ToVreg(s[1]->InputAt(0)));
- ASSERT_EQ(1U, s[1]->OutputCount());
}
}
@@ -6525,13 +6515,7 @@
m.Return(m.Load(memacc.memory_rep, memacc.result_rep, m.Parameter(0),
m.ChangeUint32ToUint64(index)));
Stream s = m.Build();
- if (immediate_shift == memacc.memory_rep.SizeInBytesLog2()) {
- ASSERT_EQ(1U, s.size());
- EXPECT_EQ(memacc.ldr_opcode, s[0]->arch_opcode());
- EXPECT_EQ(kMode_Operand2_R_LSL_I, s[0]->addressing_mode());
- EXPECT_EQ(3U, s[0]->InputCount());
- EXPECT_EQ(1U, s[0]->OutputCount());
- } else if (memacc.memory_rep == MemoryRepresentation::Simd128()) {
+ if (memacc.memory_rep == MemoryRepresentation::Simd128()) {
ASSERT_EQ(2U, s.size());
EXPECT_EQ(memacc.ldr_opcode, s[1]->arch_opcode());
EXPECT_EQ(kMode_MRR, s[1]->addressing_mode());
@@ -6585,13 +6569,7 @@
m.Parameter(2), kNoWriteBarrier);
m.Return(m.Int32Constant(0));
Stream s = m.Build();
- if (immediate_shift == memacc.memory_rep.SizeInBytesLog2()) {
- ASSERT_EQ(1U, s.size());
- EXPECT_EQ(memacc.str_opcode, s[0]->arch_opcode());
- EXPECT_EQ(kMode_Operand2_R_LSL_I, s[0]->addressing_mode());
- EXPECT_EQ(4U, s[0]->InputCount());
- EXPECT_EQ(0U, s[0]->OutputCount());
- } else if (memacc.memory_rep == MemoryRepresentation::Simd128()) {
+ if (memacc.memory_rep == MemoryRepresentation::Simd128()) {
ASSERT_EQ(2U, s.size());
EXPECT_EQ(memacc.str_opcode, s[1]->arch_opcode());
EXPECT_EQ(kMode_MRR, s[1]->addressing_mode());
Regression Test / PoC
diff --git a/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc b/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc
index d5dbdbd..f9460c4 100644
--- a/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc
+++ b/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc
@@ -278,7 +278,7 @@
{{TSUnop::kChangeUint32ToUint64, "ChangeUint32ToUint64", kArm64Mov32,
MachineType::Uint64()},
MachineType::Uint32()},
- {{TSUnop::kTruncateWord64ToWord32, "TruncateWord64ToWord32", kArm64Mov32,
+ {{TSUnop::kTruncateWord64ToWord32, "TruncateWord64ToWord32", kArchNop,
MachineType::Int32()},
MachineType::Int64()},
{{TSUnop::kChangeInt32ToFloat64, "ChangeInt32ToFloat64",
@@ -2571,17 +2571,12 @@
m.Word64ShiftRightArithmetic(p, m.Int32Constant(32)));
m.Return(t);
Stream s = m.Build();
- ASSERT_EQ(2U, s.size());
+ ASSERT_EQ(1U, s.size());
EXPECT_EQ(kArm64Asr, s[0]->arch_opcode());
ASSERT_EQ(2U, s[0]->InputCount());
EXPECT_EQ(s.ToVreg(p), s.ToVreg(s[0]->InputAt(0)));
EXPECT_EQ(32, s.ToInt64(s[0]->InputAt(1)));
ASSERT_EQ(1U, s[0]->OutputCount());
-
- EXPECT_EQ(kArm64Mov32, s[1]->arch_opcode());
- ASSERT_EQ(1U, s[1]->InputCount());
- EXPECT_EQ(s.ToVreg(s[0]->Output()), s.ToVreg(s[1]->InputAt(0)));
- ASSERT_EQ(1U, s[1]->OutputCount());
}
TEST_F(TurboshaftInstructionSelectorTest,
@@ -2593,17 +2588,12 @@
m.Word64ShiftRightLogical(p, m.Int32Constant(x)));
m.Return(t);
Stream s = m.Build();
- ASSERT_EQ(2U, s.size());
+ ASSERT_EQ(1U, s.size());
EXPECT_EQ(kArm64Lsr, s[0]->arch_opcode());
ASSERT_EQ(2U, s[0]->InputCount());
EXPECT_EQ(s.ToVreg(p), s.ToVreg(s[0]->InputAt(0)));
EXPECT_EQ(x, s.ToInt64(s[0]->InputAt(1)));
ASSERT_EQ(1U, s[0]->OutputCount());
-
- EXPECT_EQ(kArm64Mov32, s[1]->arch_opcode());
- ASSERT_EQ(1U, s[1]->InputCount());
- EXPECT_EQ(s.ToVreg(s[0]->Output()), s.ToVreg(s[1]->InputAt(0)));
- ASSERT_EQ(1U, s[1]->OutputCount());
}
}
@@ -6525,13 +6515,7 @@
m.Return(m.Load(memacc.memory_rep, memacc.result_rep, m.Parameter(0),
m.ChangeUint32ToUint64(index)));
Stream s = m.Build();
- if (immediate_shift == memacc.memory_rep.SizeInBytesLog2()) {
- ASSERT_EQ(1U, s.size());
- EXPECT_EQ(memacc.ldr_opcode, s[0]->arch_opcode());
- EXPECT_EQ(kMode_Operand2_R_LSL_I, s[0]->addressing_mode());
- EXPECT_EQ(3U, s[0]->InputCount());
- EXPECT_EQ(1U, s[0]->OutputCount());
- } else if (memacc.memory_rep == MemoryRepresentation::Simd128()) {
+ if (memacc.memory_rep == MemoryRepresentation::Simd128()) {
ASSERT_EQ(2U, s.size());
EXPECT_EQ(memacc.ldr_opcode, s[1]->arch_opcode());
EXPECT_EQ(kMode_MRR, s[1]->addressing_mode());
@@ -6585,13 +6569,7 @@
m.Parameter(2), kNoWriteBarrier);
m.Return(m.Int32Constant(0));
Stream s = m.Build();
- if (immediate_shift == memacc.memory_rep.SizeInBytesLog2()) {
- ASSERT_EQ(1U, s.size());
- EXPECT_EQ(memacc.str_opcode, s[0]->arch_opcode());
- EXPECT_EQ(kMode_Operand2_R_LSL_I, s[0]->addressing_mode());
- EXPECT_EQ(4U, s[0]->InputCount());
- EXPECT_EQ(0U, s[0]->OutputCount());
- } else if (memacc.memory_rep == MemoryRepresentation::Simd128()) {
+ if (memacc.memory_rep == MemoryRepresentation::Simd128()) {
ASSERT_EQ(2U, s.size());
EXPECT_EQ(memacc.str_opcode, s[1]->arch_opcode());
EXPECT_EQ(kMode_MRR, s[1]->addressing_mode());
Original Bug Report
ARM64 V8 Backend OOB via incorrect 32-bit index zero-extension in MemoryOperand
Flapjack (go/flapjack), an LLM-powered vulnerability discovery tool, has identified a security issue and generated a PoC.
d8 variant: ‘AsanArm64’
flags: –no-liftoff –no-wasm-lazy-compilation –omit-quit –fuzzing –disallow-unsafe-flags
Overview: The V8 ARM64 backend incorrectly optimizes 32-bit shifted memory accesses by peeling zero-extension nodes and using a 64-bit left shift addressing mode. Because 32-bit truncations do not clear the upper bits of a register on ARM64, attacker-controlled high bits are included in the address calculation. This allows an attacker to bypass the Wasm memory cage and V8 Sandbox, achieving arbitrary read/write and renderer RCE.
Affected files:
v8/src/compiler/backend/arm64/code-generator-arm64.ccv8/src/compiler/backend/arm64/instruction-selector-arm64.cc
Estimated timestamp from git blame: 2024-03-26
Root Cause
In the ARM64 Turbofan/Turboshaft backend, TruncateInt64ToInt32 is implemented as an identity operation; it reuses the virtual register without emitting code to clear the upper 32 bits. Therefore, the upper 32 bits of the 64-bit physical register remain “dirty” and attacker-controlled.
When calculating WebAssembly memory accesses, a 32-bit index is typically zero-extended to 64 bits via ChangeUint32ToUint64 before being added to the memory base pointer. However, in v8/src/compiler/backend/arm64/instruction-selector-arm64.cc, the function TryMatchLoadStoreShift explicitly peels off this ChangeUint32ToUint64 node to fold the underlying shift into the load/store instruction. It then sets the addressing mode to kMode_Operand2_R_LSL_I.
The bug is in how the code generator translates this mode. In v8/src/compiler/backend/arm64/code-generator-arm64.cc, Arm64OperandConverter::MemoryOperand implements kMode_Operand2_R_LSL_I by returning a MemOperand using an LSL shift on the full 64-bit X register:
return MemOperand(InputRegister(index + 0), InputRegister(index + 1), LSL, InputInt32(index + 2));
Because LSL does not zero-extend the index (unlike the UXTW extension mode), the dirty upper 32 bits of the register are shifted and added to the base address.
Impact
An attacker can trigger this by passing a large 64-bit value to a WebAssembly function, truncating it to 32 bits (e.g., i32.wrap_i64), and using it as a memory access index. The upper 32 bits will not be cleared, resulting in a massive 64-bit offset calculation. This bypasses the 8GB WebAssembly guard regions and the V8 Sandbox boundaries, granting arbitrary read/write access within the renderer process, which leads directly to Remote Code Execution (RCE).
Suggested Fix
The ARM64 backend must explicitly zero-extend the 32-bit index during the memory address calculation rather than relying on a 64-bit LSL.
- Introduce a new addressing mode specifically for zero-extended shifted indices (e.g.,
kMode_Operand2_R_UXTW_I) inTARGET_ADDRESSING_MODE_LIST. - In
TryMatchLoadStoreShift, if aChangeUint32ToUint64node is peeled, emit the newkMode_Operand2_R_UXTW_Imode instead ofkMode_Operand2_R_LSL_I. - In
Arm64OperandConverter::MemoryOperand, handle the new mode by constructing aMemOperandthat utilizes theUXTWextension (which correctly zero-extends the lower 32 bits before shifting):MemOperand(base, index, UXTW, shift_amount).
Evaluated with Chrome root at commit: a3f5fcb392f2902650ca2b71820e7e418787e18b
The description of the vuln is LLM-generated and can contain mistakes. Your feedback is appreciated, and will help us improve Flapjack over time. The PoC was run in a VM and it seemed to be legit - if not, let us know and we can strengthen our checker.