CVE-2025-31215
Overview
Background
- DFG/FTL abstract interpreter (AI)
- A dataflow analysis in JSC’s optimizing JIT that proves facts (types, structures) about values so later phases can drop checks and specialize code; its proofs must soundly match what the backend actually produces.
- NewArrayWithSpread / Spread
- DFG nodes that construct an array from spread elements (e.g. […x]); Spread produces the intermediate immutable butterfly and NewArrayWithSpread assembles the final array.
- JSImmutableButterfly / CopyOnWrite array
- A shared, immutable backing store (butterfly) used by copy-on-write arrays (CopyOnWriteArrayWithContiguous) so array literals can share storage until mutated.
- HavingABadTime watchpoint
- A JSGlobalObject watchpoint that fires when array prototype/indexed behavior is tampered with; while valid, the JIT may assume original, well-behaved array structures.
- PhantomNewArrayBuffer / PhantomCreateRest
- Phantom (materialization-elided) DFG nodes representing an array-literal buffer or a rest-parameter array that a Spread consumes without allocating the intermediate object.
- Structure
- JSC’s descriptor of an object’s shape and indexing type; JITted code specializes on structures, so a wrong structure assumption yields type confusion or out-of-bounds indexed access.
Root Cause Analysis
The bug is in JSC’s DFG abstract interpreter, specifically the AbstractInterpreter handling of the NewArrayWithSpread node in DFGAbstractInterpreterInlines.h. When compiling under the HavingABadTime watchpoint (i.e., array prototypes are unmodified) and the node has a single spread child whose bit is set, the FTL lowering (compileNewArrayWithSpread in FTLLowerDFGToB3.cpp) produces a CopyOnWrite contiguous array in essentially all sub-cases: a PhantomSpread of a PhantomNewArrayBuffer, a PhantomSpread of a PhantomCreateRest, or a directly-lowered immutable butterfly. The abstract interpreter, however, only proved the result was a CopyOnWrite original array (originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous)) for the narrow case where the spread wrapped a PhantomNewArrayBuffer with a contiguous indexing type; for every other single-child spread shape it fell through to a broader/less-precise structure set (originalArrayStructureForIndexingType(ArrayWithContiguous) plus the CopyOnWrite variant, or the non-original ArrayWithContiguous during allocation). This is an inconsistency between what the FTL backend actually allocates and what the abstract value model claims: the AI over- or mis-approximated the result structure relative to the concrete allocation, violating the invariant that the abstract interpreter’s proven structure must match (soundly and precisely) the structure the backend emits. Downstream optimizations that trust the AI’s structure set (for example CSE/structure checks and array-access lowering such as the .at() access in the test) can then operate on an array whose real structure differs from the assumed one.
The fix refactors compileNewArrayWithSpread so both the PhantomNewArrayBuffer and PhantomCreateRest cases go through shared helpers (createContiguousImmutableButterflyFromPhantomNewArrayBuffer / …FromPhantomCreateRest) that always yield a contiguous CopyOnWrite immutable butterfly, and correspondingly simplifies the AbstractInterpreter so that for the single-set-bit spread case it uniformly sets the node’s structure to originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous), and otherwise to arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous). This realigns the abstract structure with what is actually allocated, closing the mismatch. It is an inference (not shown verbatim in the diff) that the concrete downstream misuse is a type-confusion/OOB via an array-access node trusting the wrong structure; the diff shows the AI/lowering mismatch and a regression test that exercises spread + element write + out-of-range .at().
Attack Path
- Construct a single-child spread array literal Use a construct like […[1,2,3,4,5]] (PhantomNewArrayBuffer spread) or spreading rest arguments (PhantomCreateRest) so the DFG builds a NewArrayWithSpread node with one child whose bit vector bit 0 is set.
- Force FTL compilation under the good-time watchpoint Warm the enclosing function so it tiers up to FTL while the HavingABadTime watchpoint is still valid (the test uses –thresholdForFTLOptimizeAfterWarmUp=1000 and a 1000-iteration warmup with an inner counting loop).
- Trigger the AI/allocation structure mismatch In the spread shape not covered by the narrow AI case, the abstract interpreter models a structure set that does not match the CopyOnWrite contiguous array the FTL backend actually allocates.
- Feed the array to a structure-trusting operation Mutate and index the array (v10[2]=3 then v10.at(3745) in the test, plus a class extending a returned function to shape the graph) so an optimized array-access relies on the AI’s incorrect structure assumption.
- Provoke the fault The access proceeds under a wrong structure/indexing assumption, producing an out-of-bounds or type-confused access that crashes the WebContent process (the demonstrated outcome).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
compileNewArrayWithSpreadSource/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp |
modified | For the single-set-bit spread case, replaces inline per-shape logic with calls to the new helpers so both PhantomNewArrayBuffer and PhantomCreateRest paths uniformly build a contiguous CopyOnWrite immutable butterfly and allocate a CopyOnWriteArrayWithContiguous JSArray; unifies the fast/slow allocation and phi. |
createContiguousImmutableButterflyFromPhantomNewArrayBufferSource/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp |
added | Factored-out helper (from compileSpread) that returns the frozen contiguous immutable butterfly, or allocates a CopyOnWriteArrayWithContiguous immutable butterfly and copies the buffer's JSValues in when the source indexing type is not already contiguous. |
createContiguousImmutableButterflyFromPhantomCreateRestSource/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp |
added | Factored-out helper (from compileSpread) that builds a contiguous CopyOnWrite immutable butterfly from rest arguments, computing length/start from the inline call frame and copying values in a loop; note it now uses immutableButterflyStructures[...] indexing for the structure. |
compileSpreadSource/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp |
modified | Rewritten to delegate to the two new helpers for the PhantomNewArrayBuffer and PhantomCreateRest branches instead of inlining the allocation/copy code, so Spread and NewArrayWithSpread share the same butterfly-construction logic. |
AbstractInterpreter::executeEffects (NewArrayWithSpread case)Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h |
modified | Simplifies the abstract value: for the single-set-bit spread case it now uniformly proves originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous), and otherwise arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), removing the over-broad structure set that disagreed with what FTL allocates. |
Files Changed
JSTests/stress/dfg-ai-should-reduce-new-array-with-spread-structures.jsSource/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.hSource/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
Audit Directions
- Re-audit NewArrayWithSpread AI vs. FTL allocation parityIn DFGAbstractInterpreterInlines.h and FTLLowerDFGToB3.cpp, diff every branch of compileNewArrayWithSpread/compileSpread against the structure setForNode() calls; confirm originalArrayStructureForIndexingType(CopyOnWriteArrayWithContiguous) vs arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous) is chosen consistently for each spread shape (PhantomSpread-of-PhantomNewArrayBuffer, PhantomSpread-of-PhantomCreateRest, direct immutable butterfly).
- Sweep other array-constructing nodes for the same AI/backend divergenceGrep DFGAbstractInterpreterInlines.h for setForNode paired with originalArrayStructureForIndexingType / arrayStructureForIndexingTypeDuringAllocation on NewArray, NewArrayBuffer, NewArrayWithSize, Spread, and CreateRest, and verify each matches the concrete structure used in the corresponding FTL/DFG lowering, especially where a hasContiguous()/indexingType() special case narrows only one branch.
- Check the refactored helpers for behavior changes, not just extractionCompare createContiguousImmutableButterflyFromPhantomCreateRest’s immutableButterflyStructures[arrayIndexFromIndexingType(CopyOnWriteArrayWithContiguous) - NumberOfIndexingShapes] against the original immutableButterflyStructure(CopyOnWriteArrayWithContiguous) call to ensure the structure selection is equivalent, and audit callers of these helpers (compileSpread and compileNewArrayWithSpread) for length/MAX_STORAGE_VECTOR_LENGTH and indexing-type assumptions.
- Look for HavingABadTime-gated structure assumptions elsewhereGrep for isWatchingHavingABadTimeWatchpoint together with structure/indexing-type selection across DFG/FTL; any place that proves an ‘original array’ structure in the AI under this watchpoint but branches its concrete allocation on indexingType()/hasContiguous() is a candidate for the same mismatch.