CVE-2025-4919
Overview
Background
- `jit::ExtractLinearSum`
- an analysis routine that walks an MIR
MDefinitionarithmetic expression and returns aSimpleLinearSumof the formterm * 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 asins, with no extractable constant offset.” - `MathSpace`
- the arithmetic domain the analysis assumes for an operation:
Infinite(idealized, no wraparound) vsModulo(wrapping two’s-complementint32arithmetic). The distinction matters becausea + bin 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.
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
- 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 provesExtractLinearSum/MathSpace::Modulois the flawed analysis; the exact JS shape that drives it is standard Ion warm-up). - Trigger JIT compilation
Run the hot loop enough times that IonMonkey compiles it and invokes range analysis / bounds-check elimination, which calls
ExtractLinearSumon the index expression inModulospace. - Unsound linear sum
The old code extracts a
term + constantoffset that is not valid under wraparound, so the optimizer concludes the index is provably in-bounds and removes (or weakens) the bounds check. - 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.
- Primitive Controlled OOB read/write on a JavaScript object/array in the content process, the standard high-value SpiderMonkey exploitation primitive.
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifjs/src/jit/IonAnalysis.cpp |
modified |
Files Changed
js/src/jit/IonAnalysis.cpp
Audit Directions
- Other `MathSpace`-carrying analysesreview every consumer of
ExtractLinearSum’s result (bounds-check elimination,MBoundsCheckfolding, range analysis ofMAdd/MSub/MMod) to confirm none independently assumes non-wrapping offsets when the arithmetic is modulo. - Modulo vs infinite reasoning elsewhere in Iongrep for places that reason about
int32add/sub/offset relationships without threadingMathSpace, where wraparound could silently invalidate an inferred inequality used to drop a safety check. - Bounds-check elimination soundnessaudit the pass that removes
MBoundsChecknodes for any other path that derives an index/length relationship from arithmetic that may overflow (e.g. negative constants,INT32_MINedge cases) now that modulo sums are opaque. - Loop index recurrencesexamine induction-variable analysis that folds loop increments into linear sums, since those are the most common way wrapping index arithmetic reaches
ExtractLinearSum.
Patch
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) {