CVE-2026-28947

Overview

Advisory: Apple Security Advisory

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected Safari crash

Researchers: dr3dd

Attribute Value
CVE CVE-2026-28947
Bugzilla 310234
Component JSC
Bug Class UAF
Severity medium
Commit 76b34686210f4f67…
Advisory Apple Advisory

Root Cause Analysis

JSWebAssemblyInstance destructor called m_anchor->tearDown() after unregistering ICs and destroying baseline data. If GC ran during destruction, the anchor could be accessed after partial teardown. Fix: Moved m_anchor->tearDown() to the beginning of the destructor, before any other cleanup.

Attack Path

1. Step 1

Attacker creates and destroys WebAssembly instances repeatedly.

2. Step 2

GC triggers during JSWebAssemblyInstance destructor.

3. Step 3

Anchor is accessed by GC or concurrent thread after ICs unregistered.

4. Step 4

Use-after-free or use-after-teardown of anchor object.

5. Step 5

Memory corruption or sandbox escape.

Files Changed

Source Files

  • Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp

Test Files

  • JSTests/wasm/stress/instance-anchor.js

Patch Preview

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)