← All issues

[8] FTL ValueRepReduction type confusion in MultiGetByOffset constant cases

Severity: High | Component: JSC FTL JIT | f2c5bf8

Rated High because the observable effect is a type confusion where non-Number JSValue bit patterns are reinterpreted as IEEE 754 doubles in FTL-compiled code reachable from web content, and the confused value is deterministic (controlled by which constant appears in the MultiGetByOffset case) — exploitation via downstream array-index or bounds-check confusion is projected with confidence 0.92, with unverified claims about the specific numeric value produced and whether Load-kind cases handle conversion at emission time.

When ValueRepReductionPhase converts a MultiGetByOffset node to NodeResultDouble, Constant-kind cases must have their embedded FrozenValues converted to proper double numbers. Before the fix, the raw JSValue bit pattern of non-Number constants (e.g., undefined) was emitted as the double result instead of being converted via toNumberFromPrimitive().

Source/JavaScriptCore/dfg/DFGValueRepReductionPhase.cpp

- case MultiGetByOffset:
case GetByOffset: {
candidate->setResult(NodeResultDouble);
candidate->mergeFlags(NodeMustGenerate);
@@ ...
+ case MultiGetByOffset: {
+ MultiGetByOffsetData& data = candidate->multiGetByOffsetData();
+ for (unsigned i = 0; i < data.cases.size(); ++i) {
+ GetByOffsetMethod& method = data.cases[i].method();
+ if (method.kind() == GetByOffsetMethod::Constant) {
+ // It's possible there are non-Number constants that still predict NumberUse, e.g. undefined.
+ std::optional<double> doubleConstant = method.constant()->value().toNumberFromPrimitive();
+ method.setConstantValue(m_graph.freeze(jsDoubleNumber(*doubleConstant)));
+ }
+ }
+ candidate->setResult(NodeResultDouble);
+ resultNode = candidate;
+ break;
+ }

JSTests/stress/ftl-valuerepreduction-double-undefined.js

+for (let v0 = 0; v0 < 100; v0++) {
+ const v1 = {a: v0};
+ for (let v2 = 0; v2 < 5; v2++) {
+ v1.length += v0;
+ v0[1] = v2;
+ v2.constructor;
+ }
+ for (let v4 = 0; v4 < 50; v4++) {
+ function f5() { return v1;}
+ for (let v8 = 0; v8 < 100; v8++) {}
+ }
+}

The fix separates MultiGetByOffset from the generic GetByOffset case in the DoubleRepUse conversion. For MultiGetByOffset, it iterates all MultiGetByOffsetData::cases and, for each case whose method is GetByOffsetMethod::Constant, calls toNumberFromPrimitive() on the constant value and replaces it with a frozen jsDoubleNumber. Two new accessors support this mutation: GetByOffsetMethod::setConstantValue() and a non-const MultiGetByOffsetCase::method(). The fix unconditionally dereferences the std::optional<double> returned by toNumberFromPrimitive() via *doubleConstant without a nullopt check — safe if toNumberFromPrimitive() is total over all JS primitives.

  Before:                                After:
  MultiGetByOffset (NodeResultJS)        MultiGetByOffset (NodeResultDouble)
    case A: Load from offset               case A: Load from offset (same)
    case B: Constant(undefined)             case B: Constant(NaN) ← converted
           [raw JSValue bits emitted               [jsDoubleNumber(NaN)]
            as "double"]

MultiGetByOffset is a DFG/FTL IR node representing a polymorphic inline cache for property loads. It contains multiple cases, each matching a set of object structures and specifying a method to retrieve the value — either loading from an offset in the object (Load) or returning a constant (Constant, e.g., undefined for an absent property).

The ValueRepReductionPhase is an FTL optimization that identifies nodes producing boxed JSValues that are only ever consumed as unboxed doubles (via DoubleRep nodes). It converts these nodes to produce NodeResultDouble directly, eliminating the box-then-unbox overhead.

A FrozenValue is a compile-time snapshot of a JSValue that the JIT can embed directly into generated code. When a MultiGetByOffset constant case is emitted, the FTL materializes the frozen value's bits as the node's output. This is why constant cases require explicit conversion — unlike loads, their values are baked into the IR rather than read from memory at runtime.

In JSC's value encoding, non-double values are encoded with tag bits in the upper portion of a 64-bit word. undefined, null, true, false, and object pointers all have distinct bit patterns that do not correspond to meaningful IEEE 754 doubles.

Missing value-representation conversion of embedded constants when an IR node's result type is narrowed from JSValue to double in the FTL optimization pipeline.

Before the fix, when ValueRepReductionPhase converted a MultiGetByOffset node to NodeResultDouble, it changed the result representation without converting the constant values embedded in the node's cases. For Constant-kind cases, the FrozenValue is embedded directly into the IR. Without conversion, the raw JSValue bit pattern of the non-Number constant (e.g., undefined) would be emitted as the double result. This means the downstream JIT code interprets the JSValue encoding bits of undefined as a double-precision floating point number, producing an incorrect value rather than NaN as JavaScript semantics require.

For Load-kind cases, the value is read from memory at emission time, so the emitter likely handles the JSValue-to-double conversion during materialization (inferred from the fact that the fix only converts Constant-kind cases). But Constant-kind cases have their values baked into the IR, requiring explicit conversion.

🔒

Explores how the confused double value interacts with downstream JIT operations and whether it can be escalated beyond incorrect computation

Subscribe to read more

🔒

Multiple reusable audit patterns identified across JIT optimization phases, with concrete starting points for variant discovery

Subscribe to read more