← All issues

[1] RegExpStringIterator.next receiver type-check elision

The JIT proved a broad type and treated it as proof of the exact one.

Severity: High | Component: JSC DFG/FTL JIT | e6fd50a

High. The check the JIT skipped is exactly the check that decides whether "some object" gets read as "this iterator's internal fields". Reachable straight from renderer script; the write-primitive half still needs heap-layout control the confusion alone doesn't hand you.

JSC's optimizing JIT tiers speculate on value types and then skip runtime checks the abstract interpreter has already proven redundant, trading safety for speed on hot paths. The mid-tier DFG and top-tier FTL compilers guard a RegExpStringIteratorNext node — the operation behind next() on the iterator that String.prototype.matchAll returns — with a receiver type check before calling the native helper. That helper reads fixed-offset internal fields out of the receiver, so it is safe only when the receiver is proven to be exactly a JSRegExpStringIterator and nothing broader.

The angle: any renderer script can hand RegExpStringIterator.prototype.next a wrong-typed receiver such as an Array iterator and have the operation read that foreign object's slots as a RegExpStringIterator's regexp/subject/state pointers, a starting point for fake-object construction under heap-layout control.

SpecObjectOther type filtering is too wide for the assumptions of the operationRegExpStringIteratorNext operation. Because DFG/FTL_TYPE_CHECK use the speculated type as equivalent to the runtime check, this causes JSC to skip a type check, allowing any object to flow into the operationRegExpStringIteratorNext call.

Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp

void SpeculativeJIT::compileRegExpStringIteratorNext(Node* node)
...
- speculateCellType(node->child1(), iteratorGPR, SpecObjectOther, JSRegExpStringIteratorType);
+ speculateCellTypeWithoutTypeFiltering(node->child1(), iteratorGPR, JSRegExpStringIteratorType);
callOperation(operationRegExpStringIteratorNext, ...);

Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp

LValue iterator = lowCell(m_node->child1());
- FTL_TYPE_CHECK(jsValueValue(iterator), m_node->child1(), SpecObjectOther, isNotType(iterator, JSRegExpStringIteratorType));
+ speculate(BadType, jsValueValue(iterator), m_node->child1().node(), isNotType(iterator, JSRegExpStringIteratorType));
LValue match = vmCall(Int64, operationRegExpStringIteratorNext, weakPointer(globalObject), iterator);

JSTests/stress/regexp-string-iterator-next-wrong-receiver.js

+function makeWrongIterator(i) {
+ const array = [i, i + 1, i + 2, i + 3];
+ return array[Symbol.iterator]();
+}
+function probe(i) {
+ ...
+ if (i & 1) iter = makeWrongIterator(i); else iter = makeRegExpIterator(i);
+ ...
+ return regExpIteratorNext.call(iter);
+}
+for (let i = 0; i < testLoopCount; ++i) probe(i);

The change rewrites the receiver check emitted for the RegExpStringIteratorNext node in both JIT backends. In SpeculativeJIT::compileRegExpStringIteratorNext, the speculateCellType(node->child1(), iteratorGPR, SpecObjectOther, JSRegExpStringIteratorType) call becomes speculateCellTypeWithoutTypeFiltering(node->child1(), iteratorGPR, JSRegExpStringIteratorType). In the FTL lowering, the FTL_TYPE_CHECK(..., SpecObjectOther, isNotType(iterator, JSRegExpStringIteratorType)) becomes a raw speculate(BadType, ..., isNotType(iterator, JSRegExpStringIteratorType)). Both replacements drop the SpecObjectOther speculated-type argument, so the concrete JSRegExpStringIteratorType runtime guard is emitted unconditionally rather than gated by the abstract interpreter's needsTypeCheck. A regression test, regexp-string-iterator-next-wrong-receiver.js, is added that interleaves genuine and foreign iterators through the shared next.

Treating a proven abstract-type superset as proof of a required concrete type, allowing a JIT runtime check to be elided when the proven set is broader than the set the check enforces.

Where this lives. The DFG and FTL are JSC's optimizing JIT tiers. When a function runs hot, the baseline JIT hands it to the mid-tier DFG and eventually the top-tier FTL (which lowers through the B3 backend); both compile profile-guided, speculated machine code that assumes value types observed at runtime and bails to the interpreter (OSR exit) when an assumption fails.

The abstract interpreter and SpeculatedType. Each tier runs an abstract interpreter (AI) that propagates type information over the data-flow graph, computing for every edge a SpeculatedType — a bitmask lattice of type categories. SpecObjectOther is a broad category covering various non-final object kinds; a JSType value like JSRegExpStringIteratorType names one exact runtime cell type. Categories exist so the AI can union many concrete JSTypes into one set for fast IR-level reasoning rather than tracking each concrete type separately.

Type-check macros and elision. speculateCellType and FTL_TYPE_CHECK take a speculated-type argument and consult m_interpreter.needsTypeCheck(edge, typesPassedThrough) before emitting a runtime guard. They treat that passed SpeculatedType as equivalent to the runtime check. The speculateCellTypeWithoutTypeFiltering and raw speculate(BadType, ...) forms skip the AI consultation and always emit the concrete-type branch.

Internal fields and the operation. RegExpStringIteratorNext is the node produced for next() on the iterator returned by String.prototype.matchAll. operationRegExpStringIteratorNext reads the iterator object's fixed-offset internal fields (regexp, subject string, flags/state). These internal fields are C++-only slots — they are not JavaScript properties and cannot be read or written from script, so the operation's correctness depends entirely on the receiver actually being the C++ type whose memory layout it assumes.

The root cause is a type-check elision. The receiver guard passed SpecObjectOther as its typesPassedThrough argument, but the operation requires the far narrower invariant that the receiver is exactly a JSRegExpStringIterator. SpecObjectOther is a strict superset of that concrete type, and the macros treat "AI proved the value is within typesPassedThrough" as "the runtime check is redundant."

  Receiver edge proven SpecObjectOther on the compiled path

  Before (buggy):                    After (fixed):
  needsTypeCheck(SpecObjectOther)    speculateCellType...WithoutTypeFiltering
    -> value already in category       -> no AI consultation
    -> guard ELIDED                    -> guard ALWAYS emitted
         │                                   │
         ▼                                   ▼
  any non-final object flows in      isNotType(iterator,
  to operationRegExpStringIterator     JSRegExpStringIteratorType)
  Next unchecked                       -> OSR exit on mismatch

When AI narrows the receiver edge to SpecObjectOther — for instance because control-flow context proves the value is a non-final object — needsTypeCheck returns false, the concrete JSRegExpStringIteratorType guard is never emitted, and any object satisfying the broad category reaches operationRegExpStringIteratorNext, which then interprets that object's memory as a JSRegExpStringIterator.

The regression test drives the exact trigger:

  1. Fetch the shared iterator method: Object.getPrototypeOf("aa".matchAll(/a/g)).next.
  2. On odd iterations build a foreign receiver via makeWrongIterator (array[Symbol.iterator]()), on even iterations a genuine RegExp iterator.
  3. Call regExpIteratorNext.call(iter) in a hot loop (testLoopCount) so DFG then FTL compile the next body.
  4. If the AI narrows the receiver edge to SpecObjectOther on the compiled path, the concrete guard is elided and the foreign Array iterator flows into operationRegExpStringIteratorNext unchecked.

Exploitability is that of a JIT type confusion reachable from ordinary JavaScript. The operation reads the confused object's fields at offsets sized for the iterator's inline capacity of 2 and interprets them as regexp/string/state pointers. Under controlled heap layout, the confused object's contents could be shaped to feed subsequent match/string operations with attacker-influenced pointers, giving a controlled type-confusion primitive suitable for constructing a fake object or a relative read; realizing a full read/write would require additional grooming. Any resulting primitive is confined to the WebContent renderer sandbox, so a separate sandbox escape would still be required for system compromise.

This vulnerability weakens the JIT type system's memory-type safety inside the WebContent renderer: the security model assumes that a receiver handed to operationRegExpStringIteratorNext has been proven a genuine JSRegExpStringIterator, and the broad SpecObjectOther speculation could drop that guarantee.

This is the classic "category proves concrete type" JIT bug: a SpeculatedType category used as the second argument to a type-check macro whose runtime branch validates a single concrete JSType. Because the macros treat the speculated type as equivalent to the runtime check, any superset relationship silently becomes a skipped check. The safe idiom for operation-guarded receiver checks is the WithoutTypeFiltering / raw speculate(BadType, ...) form the fix switches to — the same pattern that already guards other internal-field-object iterator operations.