← All issues

[12] Wasm InstanceAnchor unregistered too late in destructor

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

Rated High because the diff fixes a publish/unpublish ordering inversion: the destructor freed per-instance state before tearing down the thread-safe handle the compiler thread uses to recover the instance, leaving a wide race window during which a compiler-thread profile merge could read freed baselineData slots.

~JSWebAssemblyInstance ran unregisterMirror, clearJSCallICs, and std::destroy_at loops over importFunctionInfos, tables, and baselineDatas BEFORE calling m_anchor->tearDown() — even though the anchor is what the compiler thread uses to find live instances.

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() moves to the very first action of the destructor, before any owned state is destroyed.

Publish/unpublish ordering inversion in a destructor: a thread-safe handle to the object is torn down AFTER the object's owned state is destroyed, leaving a race window where another thread can recover and dereference the partially-destroyed object.

JSWebAssemblyInstance owns per-instance Wasm state (import call link info, tables, per-function baseline profile data). Wasm::InstanceAnchor is a thread-safe-refcounted weak handle that lets the concurrent compiler thread find a live instance from a Wasm::Module; it holds the instance pointer under m_lock. Wasm::Module::m_anchors is a ThreadSafeWeakHashSet<InstanceAnchor> the compiler thread walks in createMergedProfile to combine baseline profiles before tier-up.

finishCreation explicitly publishes the instance to m_anchors (commented as "Expose it to the concurrent compiler"). Symmetric unpublishing must be the first destructor step; instead, it was last.

Exploit shape (per the regression test): instantiate the same Module many times, keep one long-lived instanceA while burying many short-lived instanceB objects on the call stack, drive instanceA's exported function in a hot loop to provoke BBQ/OMG tier-up, then call gc(). Win the race so Module::createMergedProfile -> anchor->instance()->baselineData(...) runs while a dying instance's destructor has passed the std::destroy_at over baselineDatas() but not yet reached the old end-of-function tearDown(). The compiler thread reads a destroyed RefPtr<BaselineData> slot and calls result->merge(*this, callee, *data) if non-null.

This vulnerability weakens the invariant "a JSWebAssemblyInstance is observable to the compiler thread via its anchor only while its state is intact." Standard WebAssembly APIs reach the race; a successful exploit gives a UAF primitive on per-instance Wasm baseline data inside the WebContent process.