← All reports

[JSC] Incorrect side-effect modeling for Spread(SetObjectUse)

Set spread was proven side-effect-free by a comment, not by a check

Component: JSC DFG and FTL JIT | fb8bfea

Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h

if (!m_graph.canDoFastSpread(node, forNode(node->child1()))) {
- // SetObjectUse has no side effects since we iterate directly over internal storage.
- if (node->child1().useKind() == SetObjectUse)
- didFoldClobberWorld();
- else
+ if (node->child1().useKind() == SetObjectUse) {
+ bool canFold = false;
+ JSGlobalObject* globalObject = m_graph.globalObjectFor(node->origin.semantic);
+ if (Structure* originalSetStructure = globalObject->setStructureConcurrently()) {
+ if (forNode(node->child1()).m_structure.isSubsetOf(RegisteredStructureSet(m_graph.registerStructure(originalSetStructure))))
+ canFold = true;
+ }
+ if (canFold)
+ didFoldClobberWorld();
+ else
+ clobberWorld();
+ } else
clobberWorld();
} else
didFoldClobberWorld();

The DFG abstract interpreter tracks structure proofs — compile-time guarantees that an object's hidden class has not changed — so it can elide redundant CheckStructure nodes and safely fold subsequent property accesses like GetByOffset. That optimization is only sound if no side-effecting code runs between the proof and its use. Spread(SetObjectUse) was assumed side-effect-free because JSC normally spreads Sets by iterating internal storage directly, bypassing the JS iterator protocol. The patch replaces that blanket assumption with a structure-subset test: the fold is now permitted only when the operand's proven structure is a subset of the global object's original Set structure, which guarantees no own Symbol.iterator and is watchpoint-protected; otherwise the node clobbers the world and the checks are re-emitted.

Since 313031@main, Set spreads can invoke a user-defined Symbol.iterator when the fast internal-storage path is unavailable, meaning arbitrary JavaScript — which can reshape objects — can now execute mid-spread. A JIT-compiled function could read a stale property slot after a CheckStructure was wrongly folded away, a type-confusion-shaped read reachable from ordinary script.

This is a textbook stale-structure-proof pattern, and the forward-facing hunt is for its siblings. Narrow: enumerate the other useKind-gated "no side effects" branches in executeEffects and, for each, check whether the fast-path predicate that gates them (the analogue of canDoFastSpread) can fall back to user-observable JS. Wider: the general shape is a fast-path bypass check whose failure branch reaches iterators, valueOf, toString, or proxies without the abstract interpreter being updated to match — audit every DFG node whose lowering has both an internal-storage path and a generic-protocol path. Also worth checking whether the new canFold condition's watchpoint protection on Set.prototype[Symbol.iterator] can be invalidated in ways the structure-subset check does not cover, such as realm/global-object mismatches or concurrent structure transitions during OSR. Match tell in a diff: a didFoldClobberWorld() call whose justification is a comment rather than a runtime or watchpoint-backed predicate.