72e0ec4b21 [JSC] Set MayStoreHole appropriately
Triage note: Ensures array-store profiling records hole creation, feeding JIT speculation; array indexing-mode soundness bugs are a classic OOB primitive.
Contents
The bug at a glance
This corrects ArrayProfile MayStoreHole accounting for hole-creating stores across the fast paths (directPutByVal and trySetIndexQuickly). Wrong profiling drives ’too aggressive (wrong)’ DFG/FTL speculation about whether an array store can create a hole – the soundness precondition for array-store OOB/indexing-mode bugs – but the commit frames the symptom as spurious OSR exits, so medium; the class (unsound array-store speculation) is a classic OOB primitive.
The angle is that a store past publicLength (extending the array) or into an empty array-storage slot creates a hole, and the JIT must be told via ArrayProfile::MayStoreHole. Several C++ fast paths extended the array or filled a vacant slot without ever calling setMayStoreHole, so the profile under-reported hole creation and the JIT speculated array stores as never creating holes.
Root cause
OBSERVED: ArrayProfile carries flags including OutOfBounds and MayStoreHole that summarise how an array store site has behaved, so the DFG/FTL can speculate a specialised store (e.g. one that assumes it never creates a hole and never goes out of bounds). The patch adds a public setter ArrayProfile::setMayStoreHole() that adds ArrayProfileFlag::MayStoreHole, mirroring the existing setOutOfBounds().
OBSERVED: In JITOperations.cpp directPutByVal, the previous code for int32/double/contiguous and array-storage indexing types simply ‘break’-ed when index < vectorLength and fell through to setOutOfBounds otherwise, never recording hole creation. The patch rewrites both groups. For int32/double/contiguous: if index < vectorLength, and index >= publicLength (i.e. the store extends the array, leaving a gap/hole), it calls arrayProfile->setMayStoreHole(); else if index >= vectorLength it calls setOutOfBounds(). For array-storage: it fetches storage = butterfly()->arrayStorage(), and if index < storage->vectorLength() but the target slot storage->m_vector[index] is empty (writing into a vacant slot creates a hole-fill), it calls setMayStoreHole(); otherwise setOutOfBounds().
OBSERVED: In JSObjectInlines.h trySetIndexQuickly, three cases are corrected. For contiguous int32/double, after setting the element, the existing ‘if (i >= publicLength()) setPublicLength(i+1)’ branch now also calls ‘if (arrayProfile) arrayProfile->setMayStoreHole()’ – because extending publicLength past the previous end creates holes in between. For the ArrayStorage cases, the code now takes storage = butterfly->arrayStorage(), and when writing into a slot whose existing value is empty (‘if (arrayProfile && !storage->m_vector[i]) arrayProfile->setMayStoreHole()’) records the hole before setIndexQuicklyForArrayStorageIndexingType.
INFERRED: With MayStoreHole under-set, the DFG/FTL could compile a PutByVal that assumes the store never introduces a hole, producing indexing-mode/shape assumptions that are violated at runtime. The commit says this caused ’too aggressive (wrong) optimization … causing the OSR exits’; the observable symptom the authors cite is exit churn, but unsound array-store speculation is exactly the substrate for array OOB / indexing-type confusion, so the fix closes the profiling gap that would otherwise let the JIT trust a false ’no holes’ invariant.
Key code
directPutByVal now records hole creation vs OOB for contiguous/int32/double stores (JITOperations.cpp)
case ALL_INT32_INDEXING_TYPES:
case ALL_DOUBLE_INDEXING_TYPES:
case ALL_CONTIGUOUS_INDEXING_TYPES:
if (arrayProfile) {
if (index < baseObject->butterfly()->vectorLength()) {
if (index >= baseObject->butterfly()->publicLength())
arrayProfile->setMayStoreHole();
break;
}
arrayProfile->setOutOfBounds();
}
break;
Patch walkthrough
Source/JavaScriptCore/bytecode/ArrayProfile.h— Adds the public setter setMayStoreHole() that ORs in ArrayProfileFlag::MayStoreHole, giving the fast-path C++ code a way to record hole-creating stores just as setOutOfBounds() records OOB stores.Source/JavaScriptCore/jit/JITOperations.cpp (directPutByVal)— Rewrites the int32/double/contiguous and array-storage cases so an in-bounds store that extends past publicLength, or fills a previously-empty array-storage slot, calls setMayStoreHole(), while a truly out-of-vector store calls setOutOfBounds(). Previously these in-bounds hole-creating cases recorded nothing.Source/JavaScriptCore/runtime/JSObjectInlines.h (trySetIndexQuickly)— For contiguous int32/double, the publicLength-extension branch now also sets MayStoreHole. For ArrayStorage indexing types, it fetches the storage, and when the destination slot is empty (a hole fill) sets MayStoreHole before performing the store. Ensures the quick-store path feeds the same hole information to the profile.
Background
Butterfly, vectorLength vs publicLength — A JS array’s storage (butterfly) has a vectorLength (allocated capacity) and a publicLength (the array’s logical length). Storing at an index >= publicLength but < vectorLength extends the logical length and leaves any skipped indices as holes; storing at index >= vectorLength is out of the allocated vector.
Holes and indexing modes — A hole is an absent element in an otherwise dense array. Whether an array may contain holes affects its indexing type (e.g. ArrayWithContiguous vs ArrayWithArrayStorage, and hole-vs-no-hole shapes). The JIT specialises loads/stores on these shapes, so mispredicting hole creation corrupts the shape assumptions.
ArrayProfile MayStoreHole / OutOfBounds — ArrayProfile summarises runtime behavior of an array access site. OutOfBounds marks that a store went past the vector; MayStoreHole marks that a store created a hole. The DFG/FTL read these flags to decide whether to emit a fast store that assumes no holes / in-bounds, or a general store with checks.
ArrayStorage m_vector slots — For ArrayStorage-mode arrays, elements live in storage->m_vector; an empty (zero) slot represents a hole. Writing into a previously-empty slot changes the hole population, which is why the patch checks !storage->m_vector[index] before recording MayStoreHole.
directPutByVal vs trySetIndexQuickly — directPutByVal is a JIT operation slow-ish path for indexed stores that also updates the ArrayProfile; trySetIndexQuickly is an inline fast path used from several store sites. Both must feed consistent hole/OOB information or the profile is only partially correct depending on which path executed.
Vulnerability window
- Prior state — Several fast-path store routines extended arrays or filled empty slots without recording MayStoreHole, so ArrayProfile under-reported hole creation.
- Observation — JSC engineers found MayStoreHole ’not appropriately set from C++ code,’ leading to too-aggressive DFG/FTL optimization and OSR exits (bug 317238 / rdar 179858392).
- Add setter — ArrayProfile gains a public setMayStoreHole() paralleling setOutOfBounds().
- Fix fast paths — directPutByVal and trySetIndexQuickly are updated to call setMayStoreHole on publicLength-extending contiguous stores and empty-slot array-storage stores, and setOutOfBounds on genuinely OOB stores.
- Result — The JIT sees accurate hole information and no longer speculates an unsound ’never stores a hole’ invariant for these sites.
- Landed — Committed as 315339@main on 2026-06-16, reviewed by Yijia Huang.
Triggering
OBSERVED: No test or PoC accompanies the patch. INFERRED trigger shape: repeatedly execute an indexed-store site that extends an array past its length (creating holes), e.g. ‘function f(a,i,v){ a[i] = v; }’ warmed on a dense array and then called with i beyond a[].length so the store creates holes, or an array-storage array with empty slots being filled. Pre-patch the site’s ArrayProfile would not record MayStoreHole, so the DFG/FTL could compile a store assuming no holes; the security concern is a downstream indexing-mode/shape mis-speculation, which is not demonstrated. The observable pre-patch symptom is spurious OSR exits.
Exploitation
- Reach — Warm an indexed-store site so it tiers into DFG/FTL, keeping the ArrayProfile from ever observing a hole-creating store (all early stores in-bounds/no-hole).
- Violate — Once compiled with a ’no MayStoreHole’ assumption, perform a store that actually creates a hole (extend past length, or fill an empty array-storage slot); pre-patch the profile never flagged this so the specialised store’s shape assumption is violated.
- Honest note — The commit describes the consequence as wrong optimization causing OSR exits, i.e. a soundness/perf bug; it does not ship a memory-corruption PoC. Whether a concrete OOB/type-confusion is reachable depends on the specific speculated store the profile misled, which this patch does not exhibit – treat as speculation-soundness hardening.
Detection & hunting
For defenders and SOC / detection engineers:
- OSR-exit churn on array stores —
- Hole-creating store patterns —
- Profile/reality differential —
Audit directions
- All ArrayProfile-updating store paths —
- Indexing-mode transitions —
- trySetIndexQuickly callers —
- ArrayStorage empty-slot semantics —