[12] Wasm InstanceAnchor unregistered too late in destructor
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
baselineDataslots.
~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
Patch Details
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.
Background
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.
Analysis
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.
Audit directions
- Destructors of objects published to thread-safe weak registries. Audit all callers of
ThreadSafeWeakHashSet::addandThreadSafeWeakPtrconstruction in JSC and WebCore; for each, verify the destructor's first action is to tear down the handle under the same lock the reader uses. - Publish/unpublish symmetry. Grep JSC for constructor comments containing "expose" or "publish" near
finishCreationand verify the matching destructor; auditJSWebAssemblyModule,Wasm::CalleeGroup,Wasm::BaselineData,Wasm::MergedProfile. - Compiler-thread profile merging reads through weak handles. Audit
Wasm::Module::createMergedProfilecallers that enumeratem_anchors. Check whether BBQ, OMG, ICs read per-instance state via the same handle without coordinating with the destructor. - Other thread-safe consumers of
JSWebAssemblyInstance. WasmDebugServer andm_vm->traps()mirror registration both touch the dying instance.