← All reports

[2] DFG/FTL `Spread(SetObjectUse)` realm confusion in watchpoint coverage

HighJSC DFG/FTL JITTypeConfusion

Instruction selection sank an atomic past memory.grow, onto memory already freed.

51a07f9

High. The optimizer elides the authoritative iterator-protocol check on the strength of a watchpoint that, across realms, is armed on the wrong global — so compiled code can outlive an overwrite of Set.prototype[Symbol.iterator]. Likely exploitable; escalation to addrof/fakeobj is conditional on the clobber-model coupling and structure grooming.

The DFG/FTL is WebKit's speculative JavaScript optimizer, which proves value types statically and emits fast paths guarded by watchpoints that jettison compiled code when an assumption is invalidated. The Spread(SetObjectUse) fold skips the real isIteratorProtocolFastAndNonObservable check when an upstream CheckStructure has already proven the original Set structure. Because the DFG inlines across realms, a single compiled graph can span multiple JSGlobalObjects, and the invariant at stake is that the watchpoint protecting an elision must belong to the same realm the elision was decided against.

The angle: cross-realm inlined script can take the Set spread fast path decided against realm X while the invalidation subscription sits on realm Y's watchpoint, then overwrite Set.prototype[Symbol.iterator] in X and run attacker JavaScript at a point the abstract interpreter modeled as effect-free.

Commit message: FixupPhase arms the Set iterator-protocol watchpoint on the global resolved from node->child1() — the Set operand — but the three consumers resolved the global from node->origin.semantic, the realm of the spread operation itself. The fix changes all three sites to child1()'s global.

Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h

- JSGlobalObject* globalObject = m_graph.globalObjectFor(node->origin.semantic);
+ JSGlobalObject* globalObject = m_graph.globalObjectFor(node->child1()->origin.semantic);

Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp

- auto* globalObject = m_graph.globalObjectFor(node->origin.semantic);
+ auto* globalObject = m_graph.globalObjectFor(node->child1()->origin.semantic);

Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp

- JSGlobalObject* globalObject = m_graph.globalObjectFor(m_node->origin.semantic);
+ JSGlobalObject* globalObject = m_graph.globalObjectFor(m_node->child1()->origin.semantic);

The diff changes all three consumers of the fold decision — the abstract interpreter (executeEffects), the mid-tier (SpeculativeJIT::compileSpread), and the top tier (LowerDFGToB3::compileSpread) — to resolve the iterator-protocol global from the Set operand node's semantic origin instead of the spread node's. This realigns the realm against which each backend checks setStructureConcurrently() with the realm whose watchpoint FixupPhase actually armed. Comments were added at all three sites documenting the operand-vs-operation realm distinction.

Realm/scope confusion in watchpoint coverage: a check elision is decided against one global while the invalidating watchpoint subscribes to another.

JIT tiering and speculation. JSC runs Baseline, DFG (mid-tier), and FTL (top tier) JITs; the upper tiers speculate on profile-guided type facts and OSR-exit to Baseline when a speculation fails. Watchpoints let compiled code assume a condition holds until explicitly invalidated.

The abstract interpreter. A pass over the DFG IR node graph that propagates types and structure sets along edges; "proving a type on an edge" is static analysis that lets downstream passes assume a value's shape without a runtime check.

Realms and CodeOrigin. Each JSGlobalObject is a realm. globalObjectFor(CodeOrigin) returns the realm of the code at that bytecode position. Because inlining splices callees from other realms into one graph, node->origin.semantic (the operation's position) and node->child1()->origin.semantic (the operand's producing position) can name different realms.

Set iterator-protocol watchpoint. FixupPhase arms a watchpoint that fires when Set.prototype[Symbol.iterator] or related protocol surface is mutated, jettisoning code that relied on the fast Set-spread path.

The root cause is a realm (scope) confusion whose downstream consequence is stale abstract-interpreter state across a re-entrant call.

  Fold decided against:   realm X  (node->origin.semantic — the spread)
  Watchpoint armed on:    realm Y  (child1()'s global — the Set operand)
  overwrite Set.prototype[Symbol.iterator] in realm Y  ──► no jettison

When the spread node's realm differs from the operand node's realm, the fold is decided against realm-X's setStructureConcurrently() while invalidation subscribes to realm-Y's watchpoint. Overwriting the protocol in the operand's realm never invalidates the compiled code.

The regression test drives this concretely. The operand structure matches realm B's original Set structure, so the fold is taken; the watchpoint that would fire belongs to the realm of the node that produced the Set. Overwriting Set.prototype[Symbol.iterator] in realm B therefore never invalidates. The Set is then given a nonzero deleted-entry count (add/add/delete) so the inline fast path bails to operationSpreadSet, which runs the generic iterator path and calls the attacker's generator — arbitrary JavaScript at a point the abstract interpreter modeled as effect-free. The generator installs an accessor on object o, which the surrounding function loaded before the spread and loads again after, so every structure/type fact proven about o is stale at the second load. The test pre-creates the {a} → accessor transition so the warmup structure is not a watchable leaf, ensuring Object.defineProperty does not fire a structure-transition watchpoint either.

This is a JIT soundness break: the escalation path runs through using the stale type facts on o to build a type confusion (field slots read as the wrong type), which is likely exploitable as an addrof/fakeobj starting point. That escalation is conditional on the clobber-model coupling between the re-entrant call and the loads around it, plus structure grooming of o — it is a projected direction, not established by the diff.