CVE-2026-28947
Overview
Background
- JSWebAssemblyInstance anchor
- m_anchor pins the instance for JIT/stack-scanning machinery.
- Destructor ordering
- The order in which an object releases its members; dependent members must be torn down in a safe order.
- stopThePeriphery
- Stops JS compiler threads but not wasm ones, so anchor-related state can still be touched during teardown.
Root Cause Analysis
This fixes a destruction-ordering use-after-free in JSWebAssemblyInstance’s destructor. The instance owns an m_anchor object; before the fix, ~JSWebAssemblyInstance() tore the anchor down LAST, after unregistering the stack mirror (m_vm->traps().unregisterMirror), clearing JS call inline caches (clearJSCallICs), and destroying the baseline data slots.
The patch moves m_anchor->tearDown(); m_anchor = nullptr; to the very start of the destructor. The invariant is that the anchor, which keeps the instance reachable/pinned for JIT and stack-scanning machinery, must be released before the structures that depend on it are torn down; tearing it down last meant earlier teardown steps (or concurrent GC/stack scanning during destruction) could still observe or act through the anchor after related state had begun being dismantled, yielding a use-after-free and the observed crash. Running the anchor teardown first re-establishes the correct order. The regression test warms up an instance to force JIT tiers (bury(…,500), jitPolicyScale), creates and exercises another instance, then calls gc() to drive destruction where the ordering bug manifested. INFERENCE: the exact field dereferenced after free is in the anchor/JIT teardown code not shown here; the commit establishes the ordering change.
Attack Path
- Instantiate and JIT-warm a WebAssembly instance JS builds a module and repeatedly calls an exported function so tiering/JIT and the instance anchor are set up.
- Drop references to the instance The instance goes out of scope so it becomes eligible for collection.
- Force garbage collection gc() runs ~JSWebAssemblyInstance while JIT/stack-scanning state still references the anchor.
- Use-after-free during teardown With the anchor torn down last, teardown/GC dereferences anchor-related state after it has begun being freed, crashing the process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
JSWebAssemblyInstance::~JSWebAssemblyInstanceSource/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp |
modified | Moves m_anchor->tearDown()/m_anchor=nullptr to the beginning of the destructor so the anchor is released before unregisterMirror/clearJSCallICs/baseline-data destruction, fixing the teardown-order use-after-free. |
Files Changed
JSTests/wasm/stress/instance-anchor.jsSource/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
Audit Directions
- Destructor member-release orderAudit ~JSWebAssemblyInstance and peers where an object pinning others is released after them.
- Wasm-thread lifetimes during GCLook for callees/anchors released under stopThePeriphery that wasm threads can still access.
Patch
diff --git a/JSTests/wasm/stress/instance-anchor.js b/JSTests/wasm/stress/instance-anchor.js
new file mode 100644
index 000000000000..b2b59cb78edd
--- /dev/null
+++ b/JSTests/wasm/stress/instance-anchor.js
@@ -0,0 +1,42 @@
+//@ runDefault("--jitPolicyScale=0.1")
+/*
+(module
+ (func (export "foo") (result i32)
+ i32.const 42
+ )
+)
+*/
+
+const WASM_CODE = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01, 0x03, 0x66, 0x6f, 0x6f, 0x00, 0x00, 0x0a, 0x06, 0x01, 0x04, 0x00, 0x41, 0x2a, 0x0b]);
+
+function bury(f, n) {
+ if (n === 0) {
+ return f();
+ }
+
+ return bury(f, n - 1);
+}
+
+function main() {
+ const mod = new WebAssembly.Module(WASM_CODE);
+
+ function warmUpInstanceB() {
+ const instanceB = new WebAssembly.Instance(mod);
+
+ instanceB.exports.foo();
+ }
+
+ bury(warmUpInstanceB, 500);
+
+ const instanceA = new WebAssembly.Instance(mod);
+
+ for (let i = 0; i < 500; i++)
+ instanceA.exports.foo();
+
+ gc();
+
+ print("done (should have crashed above)");
+
+}
+
+main();
diff --git a/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp b/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
index 230024a44607..88208bd7b14a 100644
--- a/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
+++ b/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
@@ -163,6 +163,11 @@ void JSWebAssemblyInstance::finishCreation(VM& vm)
JSWebAssemblyInstance::~JSWebAssemblyInstance()
{
+ if (m_anchor) {
+ m_anchor->tearDown();
+ m_anchor = nullptr;
+ }
+
m_vm->traps().unregisterMirror(m_stackMirror);
clearJSCallICs(*m_vm);
@@ -174,11 +179,6 @@ JSWebAssemblyInstance::~JSWebAssemblyInstance()
for (auto& slot : baselineDatas())
std::destroy_at(&slot);
-
- if (m_anchor) {
- m_anchor->tearDown();
- m_anchor = nullptr;
- }
}
void JSWebAssemblyInstance::destroy(JSCell* cell)