[12] Wasm InstanceAnchor unregistered too late in destructor
High로 평가된 이유는 destructor 내 publish/unpublish 순서 역전이 수정되었기 때문입니다. 수정 이전에는 per-instance 상태를 먼저 해제한 뒤, compiler thread가 instance를 찾는 데 사용하는 thread-safe handle을 나중에 해제하는 구조였습니다. 그 사이 넓은 race window가 존재해, compiler thread의 profile merge가 이미 해제된
baselineDataslot을 읽을 가능성이 있었습니다.
~JSWebAssemblyInstance는 m_anchor->tearDown()을 호출하기 전에, unregisterMirror, clearJSCallICs, 그리고 importFunctionInfos, tables, baselineDatas에 대한 std::destroy_at 루프를 먼저 실행했습니다. 그러나 compiler thread가 live instance를 찾는 데 사용하는 것이 바로 이 anchor입니다.
Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
Patch Details
m_anchor->tearDown()이 destructor의 가장 첫 번째 동작으로 이동했습니다. owned state가 해제되기 전에 먼저 호출됩니다.
Destructor 내 publish/unpublish 순서 역전 패턴. 객체의 owned state가 먼저 해제된 뒤에야 thread-safe handle이 해제되는 구조로, 다른 thread가 부분적으로 해제된 객체를 회수해 역참조할 수 있는 race window가 발생합니다.
Background
JSWebAssemblyInstance는 per-instance Wasm 상태(import call link info, table, per-function baseline profile data)를 소유합니다. Wasm::InstanceAnchor는 thread-safe-refcounted weak handle로, concurrent compiler thread가 Wasm::Module로부터 live instance를 찾는 데 사용됩니다. instance pointer는 m_lock 하에 저장됩니다. Wasm::Module::m_anchors는 ThreadSafeWeakHashSet<InstanceAnchor> 타입입니다. compiler thread는 tier-up 전에 baseline profile을 통합하기 위해 createMergedProfile에서 이 컬렉션을 순회합니다.
Analysis
finishCreation에서는 instance를 m_anchors에 명시적으로 등록합니다. 코드 주석에도 "Expose it to the concurrent compiler"라고 명시되어 있습니다. 이에 대응하는 등록 해제는 destructor의 첫 번째 동작이어야 하지만, 실제로는 마지막에 위치했습니다.
Exploit 시나리오는 regression test를 통해 확인됩니다. 같은 Module을 여러 번 인스턴스화하고, 장수명 instanceA를 유지한 채 단수명 instanceB 객체들을 call stack에 다수 쌓아둡니다. 이후 instanceA의 exported function을 hot loop으로 반복 호출해 BBQ/OMG tier-up을 유도하고, gc()를 호출합니다. race를 제대로 잡으면 다음 상황이 성립합니다. dying instance의 destructor가 baselineDatas()에 대한 std::destroy_at을 이미 통과했지만, 아직 기존 함수 끝에 위치하던 tearDown()에는 도달하지 않은 상태입니다. 이 window에서 Module::createMergedProfile → anchor->instance()->baselineData(...)가 실행됩니다. 이 시점에 compiler thread는 이미 파괴된 RefPtr<BaselineData> slot을 읽습니다. 그 값이 non-null이면 result->merge(*this, callee, *data)를 호출하게 됩니다.
이 vulnerability는 "JSWebAssemblyInstance는 상태가 온전한 동안에만 anchor를 통해 compiler thread에서 관찰 가능하다"는 invariant를 깨뜨립니다. 일반적인 WebAssembly API만으로도 이 race에 도달할 수 있습니다. exploit에 성공하면 WebContent process 내의 per-instance Wasm baseline data에 대한 UAF primitive를 얻을 수 있습니다.
Audit directions
- Destructors of objects published to thread-safe weak registries. JSC와 WebCore에서
ThreadSafeWeakHashSet::add및ThreadSafeWeakPtr생성 호출 지점 전체를 점검합니다. 각각에 대해 destructor의 첫 번째 동작이 reader가 사용하는 동일한 lock 하에 handle을 해제하는지 확인합니다. - Publish/unpublish symmetry. JSC에서
finishCreation근처에 "expose" 또는 "publish"가 포함된 생성자 주석을 검색하고, 대응하는 destructor를 확인합니다.JSWebAssemblyModule,Wasm::CalleeGroup,Wasm::BaselineData,Wasm::MergedProfile을 점검합니다. - Compiler-thread profile merging reads through weak handles.
m_anchors를 순회하는Wasm::Module::createMergedProfile호출 지점을 점검합니다. BBQ, OMG, IC가 destructor와 별도 조율 없이 동일 handle을 통해 per-instance 상태를 읽는지 확인합니다. - Other thread-safe consumers of
JSWebAssemblyInstance. WasmDebugServer와m_vm->traps()mirror 등록 모두 해제 중인 instance에 접근합니다.