1aeca6948c DFG abstract interpreter misclassifies ArraySortCompact result as SpecObjectOther
Triage note: An abstract-interpreter type proof disjoint from the runtime cell type is a JIT soundness bug that can drive downstream type-confusion miscompiles.
Contents
The bug at a glance
This is a JIT soundness bug in JavaScriptCore’s optimizing compiler: the DFG abstract interpreter and prediction-propagation phase both assign the type set SpecObjectOther to the result of ArraySortCompact, but the runtime value produced is a JSCellButterfly, whose type belongs to SpecCellOther and is disjoint from SpecObjectOther. A disjoint, wrong type proof is exactly the ingredient that downstream optimizations (CheckStructure elimination, speculation folding, type-based load/store narrowing) trust unconditionally, so it can be parlayed into a type confusion and, from there, a controlled read/write. Exploitability is raised because it is reachable from ordinary Array.prototype.sort with a JS comparator and the confusion is on a cell pointer; it is capped only in that the confused value is an internal butterfly object not directly returned to script, so an attacker must find a downstream node that consumes the bad proof to weaponize it rather than reading it out directly.
Any web page can call Array.prototype.sort() with a user comparator function; once the containing function is hot, JSC tiers it up through the DFG/FTL and lowers the sort into the ArraySortCompact/ArraySortCommit node sequence. The abstract interpreter’s claim about the type of the compaction result is baked into every later optimization decision in that compilation, so a wrong claim is web-reachable with nothing more than a sorted array and a JS callback.
Root cause
The broken invariant is the AbstractInterpreter’s core soundness contract: the abstract value attached to a node must be a conservative superset of every concrete value the node can produce at runtime. If the abstract type set and the real runtime type are disjoint, the proof is not merely imprecise, it is false, and any optimization that consumes it is miscompiled.
JSC lowers an inlined Array.prototype.sort with a JS comparator into a small pipeline of DFG nodes. ArraySortCompact is the step that walks the storage, drops holes/undefined, and yields the array’s butterfly (the out-of-line storage block, a JSCellButterfly) so the comparator loop and the final ArraySortCommit can operate on it. A butterfly is a JSCell but it is emphatically not a JSObject: SpecObjectOther denotes cells that are objects-of-other-kinds, whereas SpecCellOther denotes non-object cells. These two SpeculatedType sets do not overlap.
In DFGAbstractInterpreterInlines.h, executeEffects handled the ArraySortCompact case with setTypeForNode(node, SpecObjectOther), and DFGPredictionPropagationPhase.cpp independently seeded the same node with setPrediction(SpecObjectOther). So both the flow-insensitive prediction and the flow-sensitive abstract state agreed on a type that the value can never actually have. Because the proof says “this is an object,” any consumer is licensed to assume JSObject layout — to treat the pointer as having a Structure, inline slots, a type-info byte in the object range, etc. — none of which is true of a raw butterfly.
The fix is one token in each of the two phases: setTypeForNode(node, SpecCellOther) and setPrediction(SpecCellOther). SpecCellOther correctly and conservatively covers the JSCellButterfly, restoring the superset invariant. The test runs with –validateAbstractInterpreterState=1, which installs FTL probes that compare the AI’s claimed type against the concrete value at runtime; before the fix the probe traps deterministically because the observed butterfly is not in SpecObjectOther, which is precisely how the disjoint proof was caught.
Key code
The one-token soundness fix in the DFG abstract interpreter’s ArraySortCompact case (DFGAbstractInterpreterInlines.h); the identical change is mirrored in DFGPredictionPropagationPhase.cpp.
case ArraySortCompact:
- setTypeForNode(node, SpecObjectOther);
+ setTypeForNode(node, SpecCellOther);
break;
Patch walkthrough
Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h— In AbstractInterpreter::executeEffects, the ArraySortCompact case changes setTypeForNode(node, SpecObjectOther) to setTypeForNode(node, SpecCellOther). This is the flow-sensitive abstract state used by CSE, CheckStructure elimination, and speculation folding; correcting it to SpecCellOther makes the proof a true superset of the JSCellButterfly the node yields at runtime, so downstream nodes no longer assume JSObject layout.Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp— The prediction-propagation phase’s ArraySortCompact case changes setPrediction(SpecObjectOther) to setPrediction(SpecCellOther). Predictions drive node-type selection and speculation choices before abstract interpretation runs; aligning it with the AI change keeps both analyses consistent and prevents the compiler from picking object-shaped operations for a non-object cell.JSTests/stress/array-sort-inline-ai-cell-butterfly-validation.js— Adds a regression test that repeatedly calls a noInline’d sortIt() wrapper on fresh array copies with a JS comparator so the function tiers into the FTL. Run under –validateAbstractInterpreterState=1 and –validateDFGClobberize=1, the harness traps deterministically if the AI type for ArraySortCompact does not contain the runtime cell type, locking in the fix.
Background
DFG AbstractInterpreter and SpeculatedType — JavaScriptCore’s DFG/FTL JIT runs an abstract interpreter that computes, for each SSA node, a SpeculatedType: a bitset of the possible runtime type categories the value can hold. The compiler treats these as proofs and uses them to remove redundant type checks, fold speculations, and choose specialized machine code. The soundness rule is that the abstract type set must be a superset of every value the node can actually produce; a set disjoint from the truth is a false proof that silently authorizes wrong assumptions.
SpecObjectOther vs SpecCellOther — SpeculatedType has fine-grained cell categories. SpecObjectOther covers JSObject-kind cells that aren’t one of the well-known object types, while SpecCellOther covers cells that are not objects at all (strings live elsewhere; internal cells like butterflies fall under the non-object cell space). Crucially the two are non-overlapping, so claiming SpecObjectOther for something that is only ever SpecCellOther is a strictly wrong, not merely loose, proof.
JSCellButterfly / butterfly — A JS array’s out-of-line storage (its indexed elements and named property overflow) lives in a separate heap block called the butterfly. In the inlined sort pipeline the butterfly is materialized as a first-class cell value so the comparator loop and commit step can manipulate it. It is a GC cell but has no Structure and no JSObject header layout, which is exactly why treating it as a JSObject is dangerous.
ArraySortCompact / ArraySortCommit — When Array.prototype.sort is inlined with a JavaScript comparator, the DFG splits the operation into phases. ArraySortCompact gathers and compacts the storage and exposes the working butterfly; the comparator is then called repeatedly; ArraySortCommit writes the sorted result back. Because the comparator can trigger reentrancy and side effects, the compiler must model the intermediate values precisely, including the type of the butterfly ArraySortCompact yields.
PredictionPropagationPhase — Before abstract interpretation, a separate prediction-propagation phase seeds each node with a coarse guess of its output type based on profiling and node semantics. These predictions steer which specialized node variants and speculations the compiler selects. If the prediction and the abstract state disagree, or both agree on a wrong type, the compiler can commit to object-shaped lowering for a non-object value, which is why this patch fixes both phases in lockstep.
–validateAbstractInterpreterState — A JSC debug/validation option that inserts runtime probes comparing the abstract interpreter’s claimed type for a value against the value actually present at that program point. When the concrete value falls outside the claimed SpeculatedType, the probe traps immediately and deterministically. The regression test relies on this to surface the disjoint proof without needing a full weaponized type confusion.
Vulnerability window
- Warm-up — Web content calls Array.prototype.sort(cmp) on arrays inside a hot function; JSC’s profiling tiers the function up to the DFG and then FTL.
- Lowering — The inlined sort is compiled into the ArraySortCompact/comparator/ArraySortCommit node sequence, with ArraySortCompact producing the array’s butterfly cell.
- False proof — PredictionPropagationPhase seeds ArraySortCompact with SpecObjectOther and the AbstractInterpreter confirms SpecObjectOther in executeEffects — a type set disjoint from the butterfly’s true SpecCellOther.
- Propagation — Downstream nodes consuming the ArraySortCompact result inherit the object proof and may elide structure/type checks or select object-shaped operations for the raw butterfly.
- Miscompile — The generated code operates on the butterfly as if it were a JSObject; a load/store keyed off the false layout assumption reads or writes at the wrong offset — the type confusion.
- Detection in patch — Under –validateAbstractInterpreterState=1 the FTL probe compares the claimed type to the live butterfly and traps deterministically, which is what the added stress test exercises.
Proof of concept
The test forces sortIt() (noInline’d so its body compiles as its own function) to run testLoopCount times on fresh array copies with a JS comparator, guaranteeing the inlined-sort pipeline reaches the FTL and materializes the ArraySortCompact butterfly. Under –validateAbstractInterpreterState=1 the compiler emits a probe checking that the live value matches the AI’s claimed SpeculatedType; before the fix the probe fires because the butterfly is SpecCellOther not SpecObjectOther. It is a validation/regression harness, not a weaponized confusion — turning the disjoint proof into an R/W would require crafting a downstream node that trusts the object layout.
//@ requireOptions("--useConcurrentJIT=0", "--jitPolicyScale=0.1", "--validateAbstractInterpreterState=1", "--validateDFGClobberize=1")
// FTL ArraySortCompact returns a JSCellButterfly (SpecCellOther), not a
// JSObject. The DFG abstract interpreter (and prediction propagation) used to
// claim SpecObjectOther for this node, which is a disjoint, wrong type proof.
// With --validateAbstractInterpreterState=1 the FTL probe traps deterministically.
// This test ensures the AI/prediction match the runtime cell type so no probe fires.
function sortIt(a, cmp) { return a.sort(cmp); }
noInline(sortIt);
let cmp = (x, y) => x.idx - y.idx;
let template = [];
for (let i = 0; i < 8; i++) template.push({ idx: i });
for (let i = 0; i < testLoopCount; i++)
sortIt(template.slice(), cmp);
Exploitation
- Realize the JIT — Drive a wrapper function calling arr.sort(cmp) with a JS comparator into FTL via repeated calls, ensuring the ArraySortCompact node is emitted for a controlled array shape.
- Obtain the confused proof — The compiler now holds a false SpecObjectOther proof on the butterfly cell; the attacker must arrange the DFG graph so a consumer node uses that proof to skip a structure check or select object-typed access.
- Escalate to type confusion — A load or store lowered under the object assumption touches the butterfly at a JSObject-relative offset, producing an out-of-bounds or type-confused access on a heap cell the attacker sizes and grooms.
- Toward R/W — Classic JSC escalation from a butterfly/object confusion is to forge or overlap an array’s length/storage pointer to build addrof and arbitrary read/write; feasibility depends on which downstream node consumes the bad proof, and absent that a naive trigger stays a validation trap or crash.
Detection & hunting
For defenders and SOC / detection engineers:
- JSC validation trap —
- Crash signature —
- Version exposure —
Audit directions
- Narrow grep —
- Consistency audit —
- Internal-cell producers —
- Validation coverage —