[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.
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
Patch Details
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 어느 쪽에도 드러나지 않는 상태.
Background
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는 추적 대상에서 벗어납니다.
Analysis
store64 이후 B3는 각 element value를 dead로 판단하여 callee-saved 위치에서 자유롭게 제거할 수 있었습니다. allocateJSArray의 slow path는 GC를 유발할 수 있습니다. 이 시점에서 conservative scanner는 cell pointer를 어느 위치에서도 찾지 못하고, precise scanner 또한 아직 JSArray에 연결되지 않은 butterfly를 추적할 수 없었습니다. 결과적으로 해당 cell들이 수거 대상이 되었고, butterfly는 dangling pointer를 보유하게 되었습니다.
PoC는 new Array(N)을 할당하고 object literal을 대입하는 함수를 정의합니다. 이 함수가 FTL로 컴파일되고 allocation이 sunk되도록 유도한 뒤 (--jitPolicyScale=0.1, 수천 회 warmup), --slowPathAllocsBetweenGCs=3을 통해 GC를 강제 유발하는 slow-path allocation 위에서 materialization을 실행합니다. PoC에서 element JSObject에는 다른 live reference가 존재하지 않으므로, GC가 이를 수거합니다. 이후 allocation이 동일한 slot을 재사용하고, arr[0] === object가 해제된 slot에 새로 할당된 {} 객체와 일치하는 상황이 성립합니다. 실제 공격 환경에서라면 allocator의 fast path를 자연스럽게 소진하고 heap layout을 안정화하는 방식을 택할 것입니다. 공격자가 읽을 수 있는 JSArray element 내 dangling cell pointer는, WebContent process에서 addrof/fakeobj primitive를 거쳐 arbitrary R/W로 이어지는 전형적인 출발점입니다.
이 vulnerability는 수거 과정에서 모든 reachable cell pointer가 precise tracer 또는 conservative stack scanner 어느 한쪽에는 반드시 드러나야 한다는 GC 불변성을 깨뜨립니다. butterfly를 채우는 시점부터 JSArray가 할당되는 시점까지의 구간 동안, cell pointer는 어느 쪽에도 드러나지 않았습니다.
Audit directions
- cell pointer를 heap buffer에 저장한 뒤, 해당 buffer가 아직 어떤 reachable JSCell에도 연결되지 않은 상태에서 GC를 유발할 수 있는 호출이 이어지는 JIT 방출 코드. 동일한 패턴이 있는지 FTL의
compileMaterialize*및compileNew*lowering 전체를 점검해야 합니다. 우선compileMaterializeNewObject,compileMaterializeCreateActivation,compileNewArrayBuffer,compileNewArrayWithSize,compileNewTypedArray부터 살펴볼 필요가 있습니다. - allocation slow path를 걸쳐 cell pointer를 drop하는 backward-liveness register allocator.
FTLLowerDFGToB3.cpp와DFGSpeculativeJIT*.cpp에서allocateJSArray,allocateCell,allocateVariableSized,emitAllocateJSObject를 검색하고, 새 객체의 storage에 저장된 cell 값 LValue들이 이후에ensureStillAliveHere로 보호되는지 확인해야 합니다. - 한쪽 arm은 cell pointer를 생성하고 다른 arm은 그렇지 않은 switch arm들이 코드를 공유하는 경우.
FTLLowerDFGToB3.cpp에서case ALL_INT32_INDEXING_TYPES: case ALL_CONTIGUOUS_INDEXING_TYPES:형태의 fallthrough를 점검해야 합니다. - 역방향 탐색. FTL lowering에서 cell 타입 JSValue를 보유하는 모든
Vector<LValue>또는 LValue 집합을 찾아, GC를 유발할 수 있는 이후의 allocation 또는 runtime 호출에 걸쳐ensureStillAliveHere가 적용되어 있는지 확인해야 합니다.