7429d22f2b [JSC] Fix DFG CPS validation for inlined sort comparator
Triage note: Incorrect OSR-exit/ExitOK sequencing around inlined comparator calls is a DFG soundness bug that can corrupt exit state.
Contents
The bug at a glance
This is a DFG JIT soundness bug in the inlining of Array.prototype.sort comparators: the SetLocal queue was flushed before ExitOK was emitted, letting a node be hoisted above its producer and tripping CPS validation. CPS-form violations in the DFG are serious because incorrect def/use ordering and invalid OSR-exit state are the substrate for type-confusion miscompiles and memory corruption when validation is disabled in release builds. It is rated medium here because the observed symptom is a CPS validation trip (caught by an assertion/validator) rather than a demonstrated exploit primitive, and the fix is a two-line reordering. The comparator-inlining feature is reachable from ordinary JavaScript, which raises reachability but the patch does not establish an attacker-controlled corruption path.
Inlining Array.prototype.sort comparators (introduced by 312983@main) means a side exit restarts the entire sort call, so the exit state around the inlined comparator call must be exactly right. The bug is an ordering inversion in ByteCodeParser::handleArraySort: processSetLocalQueue() ran before emitExitOK(), allowing a SetLocal to be placed such that a node could be hoisted above its producer, violating DFG CPS (Continuation-Passing Style) form. The fix simply swaps the two calls so ExitOK is emitted first.
Root cause
In the DFG bytecode parser, handleArraySort inlines the user comparator by synthesizing a Call node (handleCall with InlineCallFrame::ArraySortComparatorCall) and then reifying the pending local-variable stores. The DFG’s SetLocal queue is a deferral mechanism: SetLocal operations are queued and flushed at well-defined points so the parser can maintain a coherent snapshot of local state for OSR exit. emitExitOK() marks the point in the node stream up to which the abstract state is a valid OSR-exit target — it asserts that the current position may legally exit to the bytecode-level continuation.
The pre-patch sequence was: handleCall(…) to inline the comparator, then processSetLocalQueue() to flush the queued SetLocals, then emitExitOK(), then cmpResult = get(tmpCmpResult). Because the SetLocal queue was flushed before ExitOK was emitted, the flushed SetLocal nodes were inserted into a region whose exit-validity had not yet been established. In CPS form the DFG maintains strict def-before-use / producer-before-consumer ordering; the commit message states the effect precisely: a node could be hoisted above its producer, tripping DFG CPS validation. In other words, flushing the queue at that point let a value-producing SetLocal (or a node depending on it) land in an order that the CPS validator rejects — the consumer appeared before its producer in the node stream for that block.
Because inlined-sort side exits restart the entire sort call, the exit state established by emitExitOK must bracket the SetLocal reification correctly: ExitOK must be emitted first so that the exit-state snapshot is anchored before the queued local stores are materialized, keeping producers ahead of consumers. The fix reorders the two statements in handleArraySort so emitExitOK() precedes processSetLocalQueue():
handleCall(…); emitExitOK(); processSetLocalQueue(); cmpResult = get(tmpCmpResult);
This guarantees the exit-OK marker is placed before the SetLocals are flushed, so the flushed nodes are ordered within an exit-valid region and the hoist-above-producer condition cannot arise. The added regression test JSTests/stress/array-sort-inline-isnan-comparator-cps.js drives Array.prototype.sort.call with isNaN as the comparator inside a nested/optimized function so the comparator is inlined, then executes deliberately convoluted follow-on code to force the parser into the affected path; the test’s value is that under debug/validation builds the pre-patch ordering trips CPS validation.
Key code
handleArraySort: emit ExitOK before flushing the SetLocal queue (DFGByteCodeParser.cpp)
auto callLinkStatus = comparatorFunction ? CallLinkStatus(CallVariant(comparatorFunction)) : CallLinkStatus(CallVariant(comparatorExecutable));
auto* callTargetNode = comparatorFunction ? jsConstant(comparatorFunction) : get(tmpComparator);
handleCall(tmpCmpResult, Call, InlineCallFrame::ArraySortComparatorCall, osrExitIndex, callTargetNode, comparatorArgcIncludingThis, newRegisterOffset, callLinkStatus, SpecBytecodeTop, nullptr);
emitExitOK();
processSetLocalQueue();
cmpResult = get(tmpCmpResult);
Patch walkthrough
Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp— In ByteCodeParser::handleArraySort, swaps the order of processSetLocalQueue() and emitExitOK() so ExitOK is emitted before the SetLocal queue is flushed. This anchors the OSR-exit-valid region before the queued local stores are materialized, preventing a node from being hoisted above its producer and keeping the DFG in valid CPS form.JSTests/stress/array-sort-inline-isnan-comparator-cps.js— New regression test. Calls Array.prototype.sort.call(arg, isNaN) from inside a nested function (process/toDict) so the comparator is inlined, then runs heavily obfuscated class/defineProperty code to exercise the parser path; on the pre-patch build this trips CPS validation in debug/validation configurations.
Background
DFG CPS form — The DFG IR has an early Continuation-Passing Style (CPS) form in which local-variable state is threaded through explicit SetLocal/GetLocal nodes and ordering constraints are strict: every value must be defined before it is used, and producers must precede consumers within a basic block. A dedicated CPS validator checks these invariants; a violation means the IR no longer faithfully represents the program’s data flow, which downstream phases assume.
emitExitOK and OSR-exit state — emitExitOK() marks the current parser position as a valid OSR-exit point — a place from which the JIT can bail out to the baseline/bytecode interpreter with a coherent reconstruction of local state. It records that the abstract exit state up to this node is consistent with the bytecode-level view. Emitting it at the wrong point means the recorded exit state does not match the actual node ordering.
processSetLocalQueue (the SetLocal deferral queue) — The DFG bytecode parser defers SetLocal operations into a queue and flushes them at controlled points via processSetLocalQueue(). Deferral lets the parser batch local stores so that exit state and value definitions are materialized in the right order. Flushing before the exit-OK marker is established places those stores in a region whose exit validity has not been anchored, enabling illegal hoisting.
Array.prototype.sort comparator inlining — 312983@main added DFG inlining of Array.prototype.sort including the user-supplied comparator, via handleArraySort and an InlineCallFrame::ArraySortComparatorCall frame. Because a side exit during the sort restarts the entire sort call, the OSR-exit state around each inlined comparator invocation must be exactly correct — any mismatch corrupts the restart. This makes the ordering of exit-OK vs SetLocal flushing safety-critical for this feature.
Hoisting above producer — In SSA/CPS-style IRs, moving a node earlier than the node that produces one of its inputs is illegal because the input would be read before it is written. Here, flushing SetLocals before ExitOK let a node be scheduled above its producer for the inlined comparator’s result/locals, which the CPS validator flags. Such def/use inversions are the classic seed of JIT miscompilations.
Vulnerability window
- Feature landed — 312983@main introduces inlining of Array.prototype.sort and its comparator; side exits restart the whole sort call.
- Latent bug — handleArraySort flushes the SetLocal queue before emitting ExitOK for the inlined comparator call path.
- Discovery — A fuzzer-style input (Array.prototype.sort.call with isNaN inside nested optimized code) trips DFG CPS validation because a node is hoisted above its producer.
- Fix — Reorder to emitExitOK() then processSetLocalQueue() in handleArraySort, anchoring the exit-valid region before flushing local stores.
- Test — Adds JSTests/stress/array-sort-inline-isnan-comparator-cps.js as a regression test that reproduces the validation trip.
- Release — Landed as 313579@main on May 20 2026 (bug 315144 / rdar://177411241), by Shu-yu Guo, reviewed by Yusuke Suzuki.
Proof of concept
Verbatim added regression test JSTests/stress/array-sort-inline-isnan-comparator-cps.js. It repeatedly calls opt() with varying argument types so the DFG tiers up and inlines Array.prototype.sort.call(arg, isNaN); the surrounding nested functions and obfuscated defineProperty body push the parser through the handleArraySort path. On a pre-patch validation build the SetLocal-before-ExitOK ordering trips DFG CPS validation.
function opt(a1) {
let array = [0, 2, ({valueOf: -Infinity, 0: 0, done: a1})];
function process(arg) {
function toDict(o) {
}
toDict((Array.prototype.sort.call(arg, (isNaN))));
}
process(array);
try {
Object.defineProperty((class a2 extends ("outer " + ("inner " + (WebAssembly.CompileError.prototype))) { get ['5']() { class a3 {
constructor() {
if (new.target) { class a4 {
};
}
}
};
} }), 'next', {set: (parseInt((function([a5 = 0x80000000, a6 = (a4.at(a1))] = []) {})))});
} catch (x) {}
}
try { opt(Infinity); } catch (y) {}
try { opt((new Map().entries())); } catch (y) {}
try { opt(Math.E); } catch (y) {}
Exploitation
- Reach — Ordinary JavaScript: force DFG tier-up of a function that calls Array.prototype.sort with an inlinable comparator, so handleArraySort runs the affected path.
- Trigger — The pre-patch ordering yields a CPS-invalid node stream (node hoisted above its producer) for the inlined comparator’s exit state.
- Escalation (unproven) — In release builds the CPS validator is compiled out; an exit-state/def-use inversion in a hot path is the general precondition for OSR-exit miscompiles and type confusion, but this patch demonstrates only a validation trip, not a concrete corruption primitive.
- Outcome — Observed as a validation assertion (debug); potential for miscompilation-driven memory corruption in release is inferred, not established. Treat as JIT soundness / crash-class pending a demonstrated primitive.
Detection & hunting
For defenders and SOC / detection engineers:
- JSC crashes in sort-comparator OSR exit —
- DFG validation assertions —
- Anomalous sort comparators —
Audit directions
- ExitOK/SetLocal ordering across handleCall sites —
- Inlined-callback exit restart correctness —
- CPS validator coverage —
- 312983@main follow-on —