← All issues

[12] Wasm InstanceAnchor unregistered too late in destructor

Severity: High | Component: JSC Wasm runtime | 76b3468

High로 평가된 이유는 destructor 내 publish/unpublish 순서 역전이 수정되었기 때문입니다. 수정 이전에는 per-instance 상태를 먼저 해제한 뒤, compiler thread가 instance를 찾는 데 사용하는 thread-safe handle을 나중에 해제하는 구조였습니다. 그 사이 넓은 race window가 존재해, compiler thread의 profile merge가 이미 해제된 baselineData slot을 읽을 가능성이 있었습니다.

~JSWebAssemblyInstancem_anchor->tearDown()을 호출하기 전에, unregisterMirror, clearJSCallICs, 그리고 importFunctionInfos, tables, baselineDatas에 대한 std::destroy_at 루프를 먼저 실행했습니다. 그러나 compiler thread가 live instance를 찾는 데 사용하는 것이 바로 이 anchor입니다.

Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp

JSWebAssemblyInstance::~JSWebAssemblyInstance()
{
+ if (m_anchor) {
+ m_anchor->tearDown();
+ m_anchor = nullptr;
+ }
+
m_vm->traps().unregisterMirror(m_stackMirror);
clearJSCallICs(*m_vm);
for (auto& slot : importFunctionInfos())
std::destroy_at(&slot);
for (auto& slot : baselineDatas())
std::destroy_at(&slot);
- if (m_anchor) {
- m_anchor->tearDown();
- m_anchor = nullptr;
- }
}

m_anchor->tearDown()이 destructor의 가장 첫 번째 동작으로 이동했습니다. owned state가 해제되기 전에 먼저 호출됩니다.

Destructor 내 publish/unpublish 순서 역전 패턴. 객체의 owned state가 먼저 해제된 뒤에야 thread-safe handle이 해제되는 구조로, 다른 thread가 부분적으로 해제된 객체를 회수해 역참조할 수 있는 race window가 발생합니다.

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_anchorsThreadSafeWeakHashSet<InstanceAnchor> 타입입니다. compiler thread는 tier-up 전에 baseline profile을 통합하기 위해 createMergedProfile에서 이 컬렉션을 순회합니다.

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::createMergedProfileanchor->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를 얻을 수 있습니다.