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)