← All issues

[JSC] Map/Set iterator next must not touch JSMapIterator/JSSetIterator directly

7937fd7

ObjectAllocationSinking is a DFG optimization that replaces heap-allocated objects with virtual SSA fields when the object doesn't escape; for it to work, every write to the object must appear as an explicit PutInternalField node. OSR exits transfer control from a JIT frame back to the interpreter mid-execution, at which point any already-mutated object state is permanent. The old MapIteratorNext advanced the iterator's internal storage pointer and bucket index as an implicit side effect hidden inside the node.

Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp

- Node* storage = addToGraph(MapIteratorNext, Edge(iterator, MapIteratorObjectUse));
- setResult(storage);
+ Node* tuple = addToGraph(MapIteratorNext, Edge(storageField), Edge(iteratedObjectField, MapObjectUse), Edge(entryField));
+ Node* newStorage = addToGraph(ExtractFromTuple, OpInfo(0), tuple);
+ newStorage->setResult(NodeResultJS);
+ Node* newEntry = addToGraph(ExtractFromTuple, OpInfo(1), tuple);
+ newEntry->setResult(NodeResultInt32);
+ addToGraph(PutInternalField, OpInfo(storageFieldIndex), mapIterator, newStorage);
+ addToGraph(PutInternalField, OpInfo(entryFieldIndex), mapIterator, newEntry);
+ FrozenValue* sentinelConst = m_graph.freezeStrong(m_graph.m_vm.orderedHashTableSentinel());
+ Node* done = addToGraph(CompareEqPtr, OpInfo(sentinelConst), newStorage);
+ setResult(done);

The refactor turns MapIteratorNext into a stateless read returning a (storage, entry) tuple; ByteCodeParser then emits explicit PutInternalField nodes to commit the new storage and entry index after all dependent key/value loads succeed, exposing the full write set to both optimizations.

Iterator sinking eliminates heap allocation on every hot for-of loop over Maps and Sets, and the deferred commit closes a real correctness gap around mid-iteration tier transitions where an OSR exit previously left the iterator in an unknown intermediate state.

When ObjectAllocationSinking sinks a JSMapIterator that later escapes, the runtime must reconstruct the live object from virtual SSA fields at the exact advancement state — a wrong field order, missing write, or incorrect storage pointer in this new materialization path would produce an iterator pointing into stale or freed bucket memory, a type-confusion or UAF primitive. The deferred commit means an OSR exit before the PutInternalFields re-executes the step on re-entry; an off-by-one in which loads are guarded before vs. after the commit could silently skip or double-visit entries. Map/Set value type speculation comes from prediction types (unlike JSStringIterator's always-String), making the sentinel/done case harder to handle cleanly — a misprediction causing an OSR exit during tuple extraction within the deferred-commit window is worth tracing through both the DFG speculative JIT and FTL B3 lowering. DFGSpeculativeJIT32_64.cpp has its own compileMapIteratorNext, so verify 32/64-bit agreement on tuple layout and commit ordering, and fuzz Map mutation (entry deletion) during sunk iteration where bucket pointers shift on rehash.