[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.
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
Patch Details
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.
Background
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.
Analysis
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.
Audit directions
- JIT-emitted code that writes cell pointers into a heap buffer that is not yet attached to any reachable JSCell, followed by a call that may trigger GC. Audit every FTL
compileMaterialize*andcompileNew*lowering for the same shape. Start withcompileMaterializeNewObject,compileMaterializeCreateActivation,compileNewArrayBuffer,compileNewArrayWithSize, andcompileNewTypedArray. - Backward-liveness register allocators dropping cell pointers across allocation slow paths. Grep
FTLLowerDFGToB3.cppandDFGSpeculativeJIT*.cppforallocateJSArray,allocateCell,allocateVariableSized,emitAllocateJSObjectand verify cell-valued LValues stored into the new object's storage are covered byensureStillAliveHereafterward. - Switch arms sharing code where one arm produces cell pointers and another does not. Audit
case ALL_INT32_INDEXING_TYPES: case ALL_CONTIGUOUS_INDEXING_TYPES:style fallthroughs inFTLLowerDFGToB3.cpp. - Inverse search. Find every
Vector<LValue>or set of LValues that holds cell-typed JSValues in the FTL lowering and confirm anensureStillAliveHerecovers them across any subsequent allocation or runtime call that can GC.