Critical CVSS 8.8 firefox OOB 🔧 Commit mapped

Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactcritical
DescriptionAn attacker was able to perform an out-of-bounds read or write on a JavaScript object by confusing array index sizes.
ComponentSpiderMonkey
Bug ClassOOB
Tracker1966614
Fix commitb03604255211 (firefox) +4/-0
CWECWE-125, CWE-787 (Out-of-bounds read, Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedManfred Paul working with Trend Micro's Zero Day Initiative
Disclosed2025-05-17

Background

`jit::ExtractLinearSum`
an analysis routine that walks an MIR MDefinition arithmetic expression and returns a SimpleLinearSum of the form term * 1 + constant (a single symbolic operand plus an integer constant). Optimizations use this normalized form to prove index/length relationships and to hoist or remove bounds checks.
`SimpleLinearSum`
the {MDefinition* term, int32_t constant} result type; SimpleLinearSum(ins, 0) means “treat the whole expression opaquely as ins, with no extractable constant offset.”
`MathSpace`
the arithmetic domain the analysis assumes for an operation: Infinite (idealized, no wraparound) vs Modulo (wrapping two’s-complement int32 arithmetic). The distinction matters because a + b in modulo space does not preserve the ordering/offset relationships that hold in infinite-precision space.

Root Cause Analysis

jit::ExtractLinearSum recurses through add/sub nodes to pull out a linear sum, and it carries a MathSpace space parameter telling it whether the arithmetic is idealized (Infinite) or wrapping (Modulo). After an early MOZ_ASSERT(space == MathSpace::Modulo || space == MathSpace::Infinite), the old code proceeded to decompose the operands lhs = ins->getOperand(0) / rhs = ins->getOperand(1) identically regardless of space, extracting constant offsets from MAdd/MSub as if the arithmetic never wrapped. That is unsound in Modulo space: when the operation is defined to wrap at the int32 boundary, the extracted term + constant relationship does not actually hold for inputs near the wrap point, so a consumer that trusts the linear sum (e.g. to conclude an index is within a length and drop the bounds check) can be fed a wrong offset. The advisory describes the observable effect precisely: an out-of-bounds read/write “by confusing array index sizes.”

The patch adds, immediately after that assertion, a hard short-circuit: if (space == MathSpace::Modulo) { return SimpleLinearSum(ins, 0); }. In modulo space the function now refuses to decompose the expression and instead returns the opaque sum (the whole node as term, constant 0), so no false constant offset is ever derived from wrapping arithmetic. Only in Infinite space does it continue into the operand-splitting logic. This restores the invariant that an extracted non-zero constant offset is only ever produced when the arithmetic is genuinely non-wrapping, preventing the downstream optimization from eliminating a bounds check it cannot actually justify. The advisory’s OOB classification matches the diff: the root is an unsound JIT analysis (a logic error in ExtractLinearSum’s handling of modulo arithmetic) whose exploitable consequence is an out-of-bounds memory access from miscomputed index bounds; I keep OOB as the class since that is the memory-safety sink the fix protects.

Key insight
ExtractLinearSum decomposed add/sub into a term + constant linear form even in MathSpace::Modulo, where wrapping int32 arithmetic makes that offset invalid; the JIT then trusted the bogus offset to eliminate an array bounds check, so the fix returns an opaque sum for modulo-space expressions.

Attack Path

  1. Script Serve JavaScript that performs integer index arithmetic which Ion compiles with modulo (wrapping int32) semantics — e.g. index expressions built from additions/subtractions that are allowed to overflow — feeding a typed-array or array element access (inferred: the diff proves ExtractLinearSum/MathSpace::Modulo is the flawed analysis; the exact JS shape that drives it is standard Ion warm-up).
  2. Trigger JIT compilation Run the hot loop enough times that IonMonkey compiles it and invokes range analysis / bounds-check elimination, which calls ExtractLinearSum on the index expression in Modulo space.
  3. Unsound linear sum The old code extracts a term + constant offset that is not valid under wraparound, so the optimizer concludes the index is provably in-bounds and removes (or weakens) the bounds check.
  4. Out-of-bounds access With the check gone, an index chosen to wrap reads or writes outside the backing buffer — the “confused array index size” out-of-bounds read/write.
  5. Primitive Controlled OOB read/write on a JavaScript object/array in the content process, the standard high-value SpiderMonkey exploitation primitive.

Impact Assessment

This is a renderer-process JIT correctness bug in IonMonkey, reachable from unprivileged web content; the CVSS vector AV:N/AC:L/PR:N/UI:R (8.8) indicates it needs the victim to load attacker JavaScript but no further privilege. The primitive is an out-of-bounds read and write arising from an eliminated bounds check under wrapping arithmetic — a strong, attacker-steerable memory-corruption primitive (this was a Pwn2Own entry by Manfred Paul via ZDI). The diff substantiates the unsound-analysis root and the bounds-check-elimination surface; converting the OOB into full code execution requires the usual renderer exploitation steps, which the diff does not show. Gating: the attacker must get the code JIT-compiled (hot path) and shape the index arithmetic so Ion treats it as modulo-space, which is achievable from pure JS.

Changed Functions

FunctionChangeNotes
if
js/src/jit/IonAnalysis.cpp
modified

Files Changed

  • js/src/jit/IonAnalysis.cpp

Audit Directions

  • Other `MathSpace`-carrying analyses
    review every consumer of ExtractLinearSum’s result (bounds-check elimination, MBoundsCheck folding, range analysis of MAdd/MSub/MMod) to confirm none independently assumes non-wrapping offsets when the arithmetic is modulo.
  • Modulo vs infinite reasoning elsewhere in Ion
    grep for places that reason about int32 add/sub/offset relationships without threading MathSpace, where wraparound could silently invalidate an inferred inequality used to drop a safety check.
  • Bounds-check elimination soundness
    audit the pass that removes MBoundsCheck nodes for any other path that derives an index/length relationship from arithmetic that may overflow (e.g. negative constants, INT32_MIN edge cases) now that modulo sums are opaque.
  • Loop index recurrences
    examine induction-variable analysis that folds loop increments into linear sums, since those are the most common way wrapping index arithmetic reaches ExtractLinearSum.
diff --git a/js/src/jit/IonAnalysis.cpp b/js/src/jit/IonAnalysis.cpp
index 2d4ede1461a..d5733f5123d 100644
--- a/js/src/jit/IonAnalysis.cpp
+++ b/js/src/jit/IonAnalysis.cpp
@@ -3726,6 +3726,10 @@ SimpleLinearSum jit::ExtractLinearSum(MDefinition* ins, MathSpace space,
   }
   MOZ_ASSERT(space == MathSpace::Modulo || space == MathSpace::Infinite);
 
+  if (space == MathSpace::Modulo) {
+    return SimpleLinearSum(ins, 0);
+  }
+
   MDefinition* lhs = ins->getOperand(0);
   MDefinition* rhs = ins->getOperand(1);
   if (lhs->type() != MIRType::Int32 || rhs->type() != MIRType::Int32) {
Loading diff…