← All issues

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

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.

🔒

Detailed GC liveness model walkthrough and an assessment of how a slow-path collection during sunken array materialization can escalate beyond a crash into something usable from JavaScript

Subscribe to read more

🔒

Four reusable audit patterns for finding sibling GC-liveness bugs across FTL materialization and allocation lowerings, each with concrete starting points

Subscribe to read more