Medium CVSS 8.8 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentJSC Wasm
Bug ClassUAF
Tracker310234
Fix commit76b34686210f (WebKit/WebKit) +47/-5
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Crediteddr3dd
Disclosed2026-05-11

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.

Key insight
The anchor must be released before the structures that depend on it; tearing it down last left it usable after related state began being freed.

Attack Path

  1. 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.
  2. Drop references to the instance The instance goes out of scope so it becomes eligible for collection.
  3. Force garbage collection gc() runs ~JSWebAssemblyInstance while JIT/stack-scanning state still references the anchor.
  4. 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

A teardown-order use-after-free surfacing as a crash during GC/destruction; medium. A UAF but driven by destruction timing rather than a directly attacker-shaped heap.

Changed Functions

FunctionChangeNotes
JSWebAssemblyInstance::~JSWebAssemblyInstance
Source/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.js
  • Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp

Audit Directions

  • Destructor member-release order
    Audit ~JSWebAssemblyInstance and peers where an object pinning others is released after them.
  • Wasm-thread lifetimes during GC
    Look for callees/anchors released under stopThePeriphery that wasm threads can still access.
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)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.