Chrome · V8 › Compiler
CVE-2025-10585
Type Confusion in V8 (Compiler)
High
patch-now
✔︎ verified
CVSS 9.8
chrome
Type Confusion
CISA KEV
⚠️ Exploited in the wild
Exploitability: High (assessed)
🔧 Commit mapped
🧬 Architecture codegen violates zero-extension and ABI invariants
Overview
High
Severity
9.8
CVSS
Yes
Exploited ITW
Fixed
Fix Status
Background
- `ZeroExtendsWord32ToWord64NoPhis`
- an Arm64 instruction-selector helper that tells the compiler whether an operation’s 32-bit result already has its upper 32 bits guaranteed to be zero in the 64-bit register, so a redundant zero-extension can be elided.
- `OverflowCheckedBinopOp`
- a Turboshaft IR node for arithmetic that also produces an overflow flag, with kinds
kSignedAdd,kSignedSub, andkSignedMuland a width given byrep. - `Int32MulWithOverflow` on Arm64
- because Arm64 has no flag-setting 32-bit multiply, the 32-bit checked multiply is lowered to a 64-bit multiplication whose overflow is detected by comparing
result.X()(full 64-bit) againstresult.W()(sign-extended low 32-bit).
Root Cause Analysis
Before the patch,
ZeroExtendsWord32ToWord64NoPhis returned true for every OverflowCheckedBinopOp whose rep was WordRepresentation::Word32(), regardless of the operation’s kind. This is correct for kSignedAdd and kSignedSub, whose Arm64 lowerings genuinely leave the upper 32 bits zeroed, but it is wrong for kSignedMul. On Arm64 the 32-bit checked multiplication is emitted as a 64-bit multiply, so the destination register can hold nonzero high bits (the full 64-bit product) rather than a clean zero-extended 32-bit value. The compiler therefore skipped a needed zero-extension and treated a dirty 64-bit register as if its top half were zero. Downstream code that consumes the value as a full 64-bit quantity then operates on garbage upper bits, producing an incorrect integer that breaks the engine’s numeric assumptions and enables type confusion.Key insight
The zero-extension guarantee depends not just on the result width (
Word32) but on how the specific operation is lowered on the target; Arm64’s flag-setting-multiply workaround silently violated the zero-extension invariant that kSignedAdd/kSignedSub satisfy.Attack Path
- Trigger the multiply
Craft JavaScript that JIT-compiles to an
Int32MulWithOverflow(OverflowCheckedBinopOpof kindkSignedMul) on an Arm64 target. - Elide the zero-extension
The buggy
ZeroExtendsWord32ToWord64NoPhisreports the 64-bit multiply result as already zero-extended, so the compiler omits the extension and leaves nonzero upper bits. - Corrupt the computed value A subsequent 64-bit use of the result reads the polluted high half, yielding an integer that diverges from what the engine’s type/range analysis expects.
- Achieve type confusion The mismatched value violates invariants (e.g., length/index or object-shape assumptions), letting an attacker force the engine to treat memory as the wrong type.
- Escalate The confusion is leveraged for out-of-bounds access and ultimately memory read/write and code execution.
Impact Assessment
This is a high-severity V8 type-confusion flaw on Arm64 that lets attacker-controlled script corrupt an integer computation and violate the engine’s type invariants, a primitive that typically leads to out-of-bounds access and remote code execution in the renderer. It was exploited in the wild as a 0-day, so working exploit chains against Arm64 Chrome existed before the fix landed.
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchsrc/compiler/backend/arm64/instruction-selector-arm64.cc |
modified |
Files Changed
src/compiler/backend/arm64/instruction-selector-arm64.cc
Audit Directions
- Width-based zero-extension assumptionsAudit every path where a
Word32result is assumed to be zero-extended toWord64; verify the guarantee holds for the specific opcode kind and its actual target lowering, not just the representation. - Target-specific arithmetic loweringsReview Arm64 (and other backend) emitters like
EmitInt32MulWithOverflowwhere a 32-bit op is implemented with a wider instruction, and confirm the selector’s assumptions about clean upper bits match the emitted code. - Overflow-checked binops in generalCross-check
kSignedAdd,kSignedSub, andkSignedMulhandling across backends for other invariants (flags, sign vs. zero extension) that may hold for some kinds but not others.
Patch
From ec6c184783824fa8f974013aae28a45c36c0112f Mon Sep 17 00:00:00 2001
From: Darius Mercadier <dmercadier@chromium.org>
Date: Tue, 16 Sep 2025 16:40:24 +0200
Subject: [PATCH] [compiler] Don't assume that upper 32-bit of Int32MulOvfCheck are 0
Because Arm64 doesn't have a flag-setting 32-bit multiplication,
which means that instead with use a 64-bit multiplication, and compare
result.X() and result.W() to check if an overflow happened. But this
leads to the upper 32-bit not being zeroed.
Fixed: 445380761
Change-Id: I31287faf37dc615695047021324e9d1d802cbec2
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6954290
Auto-Submit: Darius Mercadier <dmercadier@chromium.org>
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#102530}
---
diff --git a/src/compiler/backend/arm64/instruction-selector-arm64.cc b/src/compiler/backend/arm64/instruction-selector-arm64.cc
index 2b6d584..0aad1ef 100644
--- a/src/compiler/backend/arm64/instruction-selector-arm64.cc
+++ b/src/compiler/backend/arm64/instruction-selector-arm64.cc
@@ -2973,9 +2973,19 @@
return op.Cast<ShiftOp>().rep == WordRepresentation::Word32();
case Opcode::kComparison:
return op.Cast<ComparisonOp>().rep == RegisterRepresentation::Word32();
- case Opcode::kOverflowCheckedBinop:
- return op.Cast<OverflowCheckedBinopOp>().rep ==
- WordRepresentation::Word32();
+ case Opcode::kOverflowCheckedBinop: {
+ const OverflowCheckedBinopOp& binop = op.Cast<OverflowCheckedBinopOp>();
+ if (binop.rep != WordRepresentation::Word32()) return false;
+ switch (binop.kind) {
+ case OverflowCheckedBinopOp::Kind::kSignedAdd:
+ case OverflowCheckedBinopOp::Kind::kSignedSub:
+ return true;
+ case OverflowCheckedBinopOp::Kind::kSignedMul:
+ // EmitInt32MulWithOverflow doesn't zero-extend because Arm64 doesn't
+ // have a flag-setting int32 multiplication.
+ return false;
+ }
+ }
case Opcode::kProjection:
return ZeroExtendsWord32ToWord64NoPhis(op.Cast<ProjectionOp>().input());
case Opcode::kLoad: {
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.
References
On This Page