← All issues

[6] [JSC] Fix GC safety for sunk contiguous array materialization in FTL

FTL stored cell pointers into an unrooted butterfly, then ran a GC the precise tracer couldn't reach and the conservative scan couldn't see.

Severity: High | Component: JSC FTL JIT | e638840

Rated High because the diff plugs a GC-liveness hole at FTL: contiguous cell pointers stored into an unrooted butterfly become invisible to both precise and conservative scans across allocateJSArray's slow path; the regression test demonstrates aliasing of the supposedly-stored cell with a later allocation.

compileMaterializeNewArrayWithButterfly wrote contiguous element cell pointers into a raw butterfly, then called allocateJSArray to allocate the JSArray header. B3 backward liveness considered the cell pointers dead after the store64; the GC slow path could collect them while the butterfly was still unowned.

Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp

ObjectMaterializationData& data = m_node->objectMaterializationData();
+ Vector<LValue> contiguousElementValues;
for (unsigned i = 0; i < data.m_properties.size(); ++i) {
...
- case ALL_INT32_INDEXING_TYPES:
+ case ALL_INT32_INDEXING_TYPES: {
+ LValue value = lowJSValue(edge, ManualOperandSpeculation);
+ m_out.store64(value, butterfly, m_heaps.forIndexingType(indexingType)->at(index));
+ break;
+ }
case ALL_CONTIGUOUS_INDEXING_TYPES: {
LValue value = lowJSValue(edge, ManualOperandSpeculation);
m_out.store64(value, butterfly, m_heaps.forIndexingType(indexingType)->at(index));
+ contiguousElementValues.append(value);
break;
}
...
LValue array = allocateJSArray(indexingType, publicLength, butterfly);
+ ensureStillAliveHere(contiguousElementValues);

Contiguous element LValues are collected into contiguousElementValues; after allocateJSArray, ensureStillAliveHere(contiguousElementValues) inserts a zero-instruction B3 patchpoint that takes each value as a ColdAny operand, extending backward liveness through the allocation. The INT32 arm is split out into its own case (Int32-tagged values are not cell pointers, no GC concern). A new ensureStillAliveHere(const Vector<LValue>&) overload appends every value to one patchpoint.

Premature liveness termination of GC cell pointers stored into an unrooted heap buffer before the owning object is allocated, leaving them invisible to both precise and conservative GC scans across an allocation slow path.

Allocation sinking is a DFG optimization that defers object/array allocation; on materialization, the FTL emits element stores and the cell allocation as separate operations. For NewArrayWithButterfly, the butterfly is allocated and filled first, then allocateJSArray allocates the JSArray header. B3 computes liveness backward; a value with no further use after a point is considered dead. ensureStillAliveHere inserts a zero-instruction PatchpointValue whose only purpose is to be a formal use, keeping the register allocator from dropping it. The conservative stack scanner only sees values present in stack/registers; values dropped before a GC are not traced.

After the store64, B3 considered each element value dead and was free to drop it from any callee-saved location. allocateJSArray's slow path can trigger GC; the conservative scanner could not find the cell pointers anywhere, and the precise scanner could not trace through the not-yet-attached butterfly. The cells were eligible for collection, leaving the butterfly holding dangling pointers.

The PoC defines a function that allocates new Array(N) and assigns object literals, then arranges for the function to be FTL-compiled with the allocation sunk (--jitPolicyScale=0.1, thousand-iteration warmup), and triggers the materialization on a slow-path allocation forced to GC via --slowPathAllocsBetweenGCs=3. With the element JSObjects holding no other live references, the GC collects them; subsequent allocations reuse the slots; arr[0] === object succeeds against a freshly-allocated {} object that aliased the freed slot. In real-world grooming, an attacker would exhaust the allocator's fast path naturally and stabilize heap layout. A dangling cell pointer in an attacker-readable JSArray element is the standard stepping-stone to addrof/fakeobj and arbitrary R/W in the WebContent process.

This vulnerability weakens the GC invariant that every reachable cell pointer is visible to either the precise tracer or the conservative stack scanner during collection — for the window between butterfly population and JSArray allocation, it was visible to neither.