← All issues

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

Severity: High | Component: JSC FTL JIT | e638840

FTL에서 GC liveness hole을 막는 패치이기 때문에 High로 평가됩니다. allocateJSArray의 slow path를 걸쳐, rooting되지 않은 butterfly에 저장된 contiguous cell pointer가 precise scan과 conservative scan 어느 쪽에도 드러나지 않습니다. regression test는 저장된 것으로 간주된 cell이 이후 allocation과 aliasing되는 현상을 재현합니다.

compileMaterializeNewArrayWithButterfly는 raw butterfly에 contiguous element cell pointer를 저장한 뒤, JSArray 헤더 할당을 위해 allocateJSArray를 호출했습니다. B3의 backward liveness 분석은 store64 이후 cell pointer를 dead로 판단했고, butterfly가 아직 어느 객체에도 소속되지 않은 상태에서 GC slow path가 이를 수거할 수 있는 상황이었습니다.

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 LValue들을 contiguousElementValues에 수집합니다. allocateJSArray 호출 이후 ensureStillAliveHere(contiguousElementValues)가 삽입됩니다. 이 함수는 각 value를 ColdAny operand로 취하는 zero-instruction B3 patchpoint를 생성하여, allocation 구간에 걸쳐 backward liveness를 연장합니다. INT32 arm은 별도의 case로 분리되었습니다. Int32-tagged value는 cell pointer가 아니므로 GC 측면에서 문제가 되지 않기 때문입니다. 새로 추가된 ensureStillAliveHere(const Vector<LValue>&) overload는 모든 value를 하나의 patchpoint에 추가합니다.

소유 객체가 할당되기 전, rooting되지 않은 heap buffer에 저장된 GC cell pointer의 조기 liveness 종료. allocation slow path를 걸쳐 precise scan과 conservative GC scan 어느 쪽에도 드러나지 않는 상태.

Allocation sinking은 DFG의 최적화 기법으로, 객체와 배열의 할당 시점을 뒤로 미룹니다. materialization 과정에서 FTL은 element 저장과 cell 할당을 별개의 연산으로 방출하며, NewArrayWithButterfly의 경우 butterfly를 먼저 할당하고 채운 뒤 allocateJSArray가 JSArray 헤더를 할당하는 순서를 따릅니다. B3는 backward 방식으로 liveness를 계산하며, 어느 지점 이후 사용이 없는 value는 dead로 간주됩니다. ensureStillAliveHere는 zero-instruction PatchpointValue를 삽입하는데, 그 유일한 목적은 formal use로 등록하여 register allocator가 해당 value를 drop하지 못하게 하는 것입니다. conservative stack scanner는 stack과 register에 존재하는 value만 탐색하므로, GC 이전에 drop된 value는 추적 대상에서 벗어납니다.

store64 이후 B3는 각 element value를 dead로 판단하여 callee-saved 위치에서 자유롭게 제거할 수 있었습니다. allocateJSArray의 slow path는 GC를 유발할 수 있습니다. 이 시점에서 conservative scanner는 cell pointer를 어느 위치에서도 찾지 못하고, precise scanner 또한 아직 JSArray에 연결되지 않은 butterfly를 추적할 수 없었습니다. 결과적으로 해당 cell들이 수거 대상이 되었고, butterfly는 dangling pointer를 보유하게 되었습니다.

🔒

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

더 확인하려면 구독해 주세요

🔒

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

더 확인하려면 구독해 주세요