← All issues

[10] [JSC] StringAt should respect arrayMode in CSE

Severity: High | Component: JSC DFG JIT | 1f4e4ca

Rated High because the diff fixes an unsound CSE on String.prototype.at: the GCSE key omitted arrayMode, allowing the optimizer to merge an in-bounds (returns String) computation with an OOB (returns undefined) computation — a type confusion at JIT-IR level reachable from web JS.

StringAt fell through to StringCharAt's def(PureValue(node)) clause. charAt is type-stable (always returns a string, empty on OOB), but at returns either a string or undefined depending on bounds.

Source/JavaScriptCore/dfg/DFGClobberize.h

case StringAt:
+ def(PureValue(node, node->arrayMode().asWord()));
+ return;
case StringCharAt:
def(PureValue(node));
return;

StringAt gets its own case calling def(PureValue(node, node->arrayMode().asWord())), folding ArrayMode into the CSE key so nodes with different array modes are no longer merged.

CSE key omits a mode discriminator that controls the operation's return type, allowing the optimizer to merge two computations with incompatible output-type contracts.

clobberize() produces read/write/def summaries that drive CSE for pure nodes. PureValue is the CSE key for side-effect-free nodes; by default it hashes (opcode, child edges), but an extra discriminator can be folded in via PureValue(node, word). ArrayMode is the DFG's per-node specialisation of an indexed operation, derived from per-bytecode array profiles. String.prototype.at(i) returns the code unit string at i if in range and undefined otherwise — distinct from charAt, which returns the empty string for OOB.

Two s.at(i) calls in the same function can be specialised differently — one as String in-bounds, another as Generic/OOB-observing. With the shared PureValue(node) key omitting ArrayMode, CSE happily merged a mode-A StringAt with a mode-B StringAt using the same string/index inputs.

The surviving node's specialised lowering (and the type abstract-interpreted by downstream nodes) was then applied to a usage expecting the other mode's return-type contract — a String-vs-Undefined type confusion. The regression test warms up opt("hello", 1) 20000 times (always in-bounds), then calls opt("hello", 100) 200 times (always OOB) under --jitPolicyScale=0.1. A value typed as String by JIT tracking but runtime-tagged as Undefined is the standard DFG type-confusion precursor.

This vulnerability weakens DFG's type-system invariant inside the WebContent process: two CSE-merged nodes are supposed to share output type, and they didn't.