eba64ef44d [JSC] Array rematerialization should know how to have a bad time
Triage note: Rematerializes sunk arrays with the original structure and clears holes in the correct backing store, fixing a JIT layout/type-confusion.
Contents
The bug at a glance
An FTL OSR-exit rematerialization used the wrong butterfly/structure when the VM ‘had a bad time’ between compile and exit, treating an ArrayStorage array as if it had a contiguous butterfly. That is a JIT layout/type-confusion in a JS-reachable path (defining an indexed accessor on Array.prototype forces the bad time), a classic high-severity JS engine bug with a shipped regression test.
PhantomNewArrayWithButterfly rematerialization always built a contiguous butterfly via arrayStructureForIndexingTypeDuringAllocation. If isHavingABadTime() became true after FTL compilation, that allocator now returns SlowPutArrayStorage structures, so the sunk array was created/interpreted with mismatched layout. The fix rematerializes with the original (pre-bad-time) structure and then explicitly switchToSlowPutArrayStorage after populating, and clears holes in the correct backing store.
Root cause
When the FTL ‘sinks’ an array allocation (PhantomNewArrayWithButterfly) it defers materialization until an OSR exit; operationMaterializeObjectInOSR then rebuilds the real JSArray with its butterfly. The old code chose the structure via globalObject->arrayStructureForIndexingTypeDuringAllocation(indexingType). This allocator is sensitive to the global object ‘having a bad time’: once isHavingABadTime() is true (e.g. an indexed accessor is defined on Array.prototype/Object.prototype), the engine migrates all array structures to SlowPutArrayStorage so that indexed stores consult prototype setters.
If the bad time began after the FTL code was compiled but before this OSR exit, arrayStructureForIndexingTypeDuringAllocation now returns a SlowPutArrayStorage structure. The rematerialization code, however, still populated the object as if it had a contiguous butterfly (writing directly into contiguous()/contiguousDouble() slots and handling holes as contiguous), producing an object whose structure claims ArrayStorage but whose butterfly is laid out and filled as contiguous - a layout/type confusion. Later fast-path array code interpreting the ArrayStorage butterfly reads garbage (m_vector/m_publicLength/m_numValuesInVector overlaid on contiguous data), which is memory-unsafe.
The fix: operationMaterializeObjectInOSR now allocates with globalObject->originalArrayStructureForIndexingType(indexingType) - the pre-bad-time structure - so the butterfly is always the expected non-ArrayStorage contiguous shape while it is being populated with the materialization’s properties and hole sentinels. After population, if (globalObject->isHavingABadTime()) result->switchToSlowPutArrayStorage(vm); converts the fully-formed contiguous array to SlowPutArrayStorage properly (an ASSERT confirms the current allocator would indeed yield SlowPutArrayStorage). Correspondingly, operationPopulateObjectInOSR gains a branch: when hasAnyArrayStorage(indexingType) and the value is a hole (!value), it clears the ArrayStorage vector slot (arrayStorage()->m_vector[index].clear()) instead of writing a contiguous hole, matching the post-conversion layout.
Key code
Rematerialize with original structure, convert to SlowPutArrayStorage after (FTLOperations.cpp)
case PhantomNewArrayWithButterfly: {
// Rematerialized butterflies are always non-ArrayStorage. However, isHavingABadTime could
// have become true between the FTL compilation and the rematerialization, ...
Structure* structure = globalObject->originalArrayStructureForIndexingType(materialization->indexingType());
...
if (globalObject->isHavingABadTime()) [[unlikely]] {
#if ASSERT_ENABLED
Structure* originalStructure = globalObject->arrayStructureForIndexingTypeDuringAllocation(materialization->indexingType());
ASSERT(!originalStructure || hasSlowPutArrayStorage(originalStructure->indexingType()));
#endif
result->switchToSlowPutArrayStorage(vm);
}
return result;
}
Patch walkthrough
Source/JavaScriptCore/ftl/FTLOperations.cpp— In operationMaterializeObjectInOSR’s PhantomNewArrayWithButterfly case, the structure is now taken from globalObject->originalArrayStructureForIndexingType(materialization->indexingType()) instead of arrayStructureForIndexingTypeDuringAllocation, guaranteeing a non-ArrayStorage butterfly during population. After the butterfly is filled, a new blockif (globalObject->isHavingABadTime()) { <ASSERT the allocator would give SlowPutArrayStorage> result->switchToSlowPutArrayStorage(vm); }converts the array to SlowPutArrayStorage. In operationPopulateObjectInOSR, a new branchelse if (hasAnyArrayStorage(array->indexingType()) && !value) array->butterfly()->arrayStorage()->m_vector[index].clear();clears holes in the ArrayStorage vector when the array has been converted, rather than through the contiguous butterfly.JSTests/stress/ftl-osr-exit-phantom-new-array-with-butterfly-having-a-bad-time.js— Regression test: warms up opt() (which builds and fills a double array then gc()s) to get FTL to sink the array, then sets trigger so cb() defines an indexed getter on Array.prototype (forcing a bad time) during the optimized run, driving an OSR exit that rematerializes the sunk array while the VM is having a bad time.
Background
Sinking / PhantomNewArrayWithButterfly — An FTL optimization that elides an array allocation whose result may not be needed, recording enough metadata (a ‘materialization’ with indexing type and properties) to reconstruct the array lazily if an OSR exit occurs. Rematerialization must faithfully rebuild the array with the layout the rest of the VM expects.
Having a bad time — A global-object state entered when script installs an indexed accessor on a shared prototype (e.g. Array.prototype). To make all indexed stores consult those accessors, the engine migrates array structures to SlowPutArrayStorage. Any code that assumes contiguous/fast layout after this point is unsound.
arrayStructureForIndexingTypeDuringAllocation vs originalArrayStructureForIndexingType — The former returns the structure currently used for new allocations, which becomes SlowPutArrayStorage during a bad time; the latter returns the original (fast) structure regardless. The fix deliberately uses the original so the butterfly is populated in the known contiguous shape before an explicit conversion.
switchToSlowPutArrayStorage — A JSArray method that reallocates/reinterprets the butterfly into SlowPutArrayStorage form, updating m_vector/m_numValuesInVector/m_publicLength consistently. Doing the conversion after full contiguous population avoids the layout mismatch the bug produced.
Vulnerability window
- Compile — FTL compiles opt(), sinking a NewArrayWithButterfly; the array is not materialized eagerly.
- Bad time — During the optimized run, cb() defines an indexed getter on Array.prototype, flipping the global object into having a bad time and migrating array structures to SlowPutArrayStorage.
- OSR exit — An exit forces operationMaterializeObjectInOSR to rebuild the sunk array; the old allocator now returns a SlowPutArrayStorage structure.
- Layout confusion — The array is populated as if contiguous while its structure claims ArrayStorage, so subsequent array fast paths misread the butterfly.
- Fix — Materialize with the original contiguous structure, then switchToSlowPutArrayStorage; populate clears holes in the ArrayStorage vector when converted.
Proof of concept
VERBATIM added JSTests/stress/ftl-osr-exit-phantom-new-array-with-butterfly-having-a-bad-time.js. opt() builds and fully fills a 5-element double array so FTL sinks the allocation; after 1000 warmup iterations it is FTL-compiled. Setting trigger makes cb() define an indexed getter at index 0 on Array.prototype during the optimized call, entering a bad time; the ensuing OSR exit rematerializes the sunk array while structures have moved to SlowPutArrayStorage, exercising the previously-mismatched layout path. Pre-patch this produces a corrupt/type-confused array.
//@ runDefault("--jitPolicyScale=0.1")
let trigger = false;
function cb() {
if (trigger) {
Object.defineProperty(Array.prototype, 0, {
get() { return 42; }, configurable: true
});
}
}
noInline(cb);
function collect() { gc(); }
noInline(collect);
function opt() {
let a = new Array(5);
a[0] = 1.1;
a[1] = 2.2;
a[2] = 3.3;
a[3] = 4.4;
a[4] = 5.5;
cb();
collect();
return a[0] + a[1] + a[2] + a[3] + a[4];
}
noInline(opt);
for (let i = 0; i < 1000; i++)
opt();
trigger = true;
opt();
gc();
Exploitation
- Reachability — Entirely script-driven: JIT tiering plus Object.defineProperty on Array.prototype are ordinary JS; –jitPolicyScale merely speeds tiering in the test.
- Confusion setup — Force FTL to sink an array, then trigger a bad time mid-run so the OSR-exit rematerialization builds an array whose declared structure (ArrayStorage) disagrees with its populated butterfly (contiguous).
- Primitive — Type/layout confusion between contiguous and ArrayStorage butterflies. Reading/writing such an array through fast paths misinterprets m_vector/length fields; a full exploit would leverage the mismatch into OOB access, but the shipped test only demonstrates the corruption/soundness failure, not a weaponized primitive.
Detection & hunting
For defenders and SOC / detection engineers:
- Crash/assert in array fast paths after OSR exit —
- Structure/indexingType mismatch assertions —
- having-a-bad-time + OSR exit coincidence —
Audit directions
- All Phantom materializations —
- Bad-time transitions vs cached structures —
- Hole handling by backing store —
- switchToSlowPutArrayStorage callers —