CVE-2026-43712
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Kwak Kiyong, Song nuri, Tristan Madani (@TristanInSec) from Talence Security
Key insight. Retaining only the directly-named TypeDefinition — not the transitive closure of types it references — left dangling TypeIndex values in long-lived Globals/Tables once the defining module was collected.
Patch Details
diff --git a/JSTests/wasm/gc/transitive-type-retention-global.js b/JSTests/wasm/gc/transitive-type-retention-global.js
new file mode 100644
index 000000000000..ff5d2608adf8
--- /dev/null
+++ b/JSTests/wasm/gc/transitive-type-retention-global.js
@@ -0,0 +1,24 @@
+// Regression test: Wasm Global must retain transitive TypeDefinition dependencies.
+// A global typed (ref null $f) where $f references struct type $s must keep $s alive
+// even after the module is collected.
+
+import { instantiate } from "./wast-wrapper.js";
+
+(function() {
+ let inst = instantiate(`
+ (module
+ (rec (type $s (struct (field i32) (field i32) (field i32) (field i32))))
+ (rec (type $f (func (param (ref null $s)) (result (ref null $s)))))
+ (global (export "g") (mut (ref null $f)) (ref.null $f))
+ )
+ `);
+
+ let g = inst.exports.g;
+ inst = null;
+
+ gc();
+ gc();
+
+ g.value;
+ g.value = null;
+})();
diff --git a/JSTests/wasm/gc/transitive-type-retention-table.js b/JSTests/wasm/gc/transitive-type-retention-table.js
new file mode 100644
index 000000000000..30886e1a7692
--- /dev/null
+++ b/JSTests/wasm/gc/transitive-type-retention-table.js
@@ -0,0 +1,25 @@
+// Regression test: Wasm Table must retain transitive TypeDefinition dependencies.
+// A table typed (ref null $f) where $f references struct type $s must keep $s alive
+// even after the module is collected.
+
+import { instantiate } from "./wast-wrapper.js";
+
+(function() {
+ let inst = instantiate(`
+ (module
+ (rec (type $s (struct (field i32) (field i32) (field i32) (field i32))))
+ (rec (type $f (func (param (ref null $s)) (result (ref null $s)))))
+ (table (export "tbl") 10 10 (ref null $f))
+ )
+ `);
+
+ let tbl = inst.exports.tbl;
+ inst = null;
+
+ gc();
+ gc();
+
+ tbl.get(0);
+ tbl.set(0, null);
+ tbl.grow(0, null);
+})();
diff --git a/Source/JavaScriptCore/wasm/WasmGlobal.h b/Source/JavaScriptCore/wasm/WasmGlobal.h
index 08689a3bde2e..ae557c69fbb7 100644
--- a/Source/JavaScriptCore/wasm/WasmGlobal.h
+++ b/Source/JavaScriptCore/wasm/WasmGlobal.h
@@ -32,6 +32,7 @@
#include <JavaScriptCore/SlotVisitorMacros.h>
#include <JavaScriptCore/WasmFormat.h>
#include <JavaScriptCore/WasmLimits.h>
+#include <JavaScriptCore/WasmTypeDefinition.h>
#include <JavaScriptCore/WriteBarrier.h>
#include <wtf/Ref.h>
#include <wtf/TZoneMalloc.h>
@@ -99,25 +100,28 @@ public:
private:
Global(Wasm::Type type, Wasm::Mutability mutability, uint64_t initialValue)
: m_type(type)
- , m_typeDefinition(TypeInformation::getRef(type.index))
, m_mutability(mutability)
{
ASSERT(m_type != Types::V128);
+ if (auto typeDefinition = TypeInformation::getRef(type.index))
+ m_typeDependencies.emplace(Ref { *typeDefinition });
m_value.m_primitive = initialValue;
}
Global(Wasm::Type type, Wasm::Mutability mutability, v128_t initialValue)
: m_type(type)
- , m_typeDefinition(TypeInformation::getRef(type.index))
, m_mutability(mutability)
{
ASSERT(m_type == Types::V128);
+ if (auto typeDefinition = TypeInformation::getRef(type.index))
+ m_typeDependencies.emplace(Ref { *typeDefinition });
m_value.m_vector = initialValue;
}
Wasm::Type m_type;
- // If m_type came from a TypeDefinition, the following retains the definition to prevent a dangling m_type.
- RefPtr<const Wasm::TypeDefinition> m_typeDefinition;
+ // If m_type came from a TypeDefinition, the following retains the definition
+ // and all transitively reachable types to prevent dangling TypeIndex values.
+ std::optional<WebAssemblyGCTypeDependencies> m_typeDependencies;
Wasm::Mutability m_mutability;
JSWebAssemblyGlobal* m_owner { nullptr };
Value m_value;
diff --git a/Source/JavaScriptCore/wasm/WasmTable.cpp b/Source/JavaScriptCore/wasm/WasmTable.cpp
index be6b6df2e054..0a41b0da1de7 100644
--- a/Source/JavaScriptCore/wasm/WasmTable.cpp
+++ b/Source/JavaScriptCore/wasm/WasmTable.cpp
@@ -86,10 +86,11 @@ Table::Table(uint32_t initial, std::optional<uint32_t> maximum, Type wasmType, T
: m_maximum(maximum)
, m_type(type)
, m_wasmType(wasmType)
- , m_wasmTypeDefinition(TypeInformation::getRef(wasmType.index))
, m_isFixedSized(maximum && maximum.value() == initial)
, m_owner(nullptr)
{
+ if (auto typeDefinition = TypeInformation::getRef(wasmType.index))
+ m_typeDependencies.emplace(Ref { *typeDefinition });
setLength(initial);
ASSERT(!m_maximum || *m_maximum >= m_length);
}
diff --git a/Source/JavaScriptCore/wasm/WasmTable.h b/Source/JavaScriptCore/wasm/WasmTable.h
index bf9c237323a0..0e1494cf46aa 100644
--- a/Source/JavaScriptCore/wasm/WasmTable.h
+++ b/Source/JavaScriptCore/wasm/WasmTable.h
@@ -33,6 +33,7 @@
#include <JavaScriptCore/WasmCallee.h>
#include <JavaScriptCore/WasmFormat.h>
#include <JavaScriptCore/WasmLimits.h>
+#include <JavaScriptCore/WasmTypeDefinition.h>
#include <JavaScriptCore/WriteBarrier.h>
#include <wtf/MallocPtr.h>
#include <wtf/Ref.h>
@@ -106,8 +107,9 @@ protected:
NO_UNIQUE_ADDRESS const std::optional<uint32_t> m_maximum;
const TableElementType m_type;
Type m_wasmType;
- // If m_wasmType came from a TypeDefinition, the following retains the definition to prevent a dangling m_wasmType.
- RefPtr<const TypeDefinition> m_wasmTypeDefinition;
+ // If m_wasmType came from a TypeDefinition, the following retains the definition
+ // and all transitively reachable types to prevent dangling TypeIndex values.
+ std::optional<WebAssemblyGCTypeDependencies> m_typeDependencies;
bool m_isFixedSized { false };
JSWebAssemblyTable* m_owner;
};
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43712 |
| Bugzilla | 314235 |
| Component | JSC |
| Bug Class | UAF |
| Severity | medium |
| Commit | e96472d9cab94158… |
| Advisory | Apple Advisory |
Terminology
- Wasm GC typed references — WebAssembly GC lets values be typed references like (ref null $t) that point at struct/array/func TypeDefinitions identified by a TypeIndex.
- TypeDefinition / TypeIndex — A TypeDefinition is the interned description of a wasm type; a Wasm::Type stores a TypeIndex into that table, which must stay valid as long as any value of that type exists.
- Transitive type retention — A type can reference other types (a func type refers to its parameter struct), so keeping a type valid requires retaining the whole reachable graph, not just the named definition.
- WebAssemblyGCTypeDependencies — The helper introduced by the fix that holds strong references to a TypeDefinition and every type transitively reachable from it.
Root Cause Analysis
A WebAssembly Global or Table whose element type is a GC reference (e.g. (ref null $f)) stores that type as a Wasm::Type carrying a TypeIndex. To keep the TypeIndex valid after the defining module is gone, WasmGlobal and WasmTable retained the type with a single RefPtr<const Wasm::TypeDefinition> m_typeDefinition = TypeInformation::getRef(type.index). That pins only the directly-named TypeDefinition. But a TypeDefinition can transitively reference other types — in the regression tests, function type $f references struct type $s — and those transitively-reachable definitions were not retained by anyone once the module and the strong references it held were collected. The Global/Table then still held a Wasm::Type whose TypeIndex pointed at a freed $s definition. A later operation on the global or table (reading or writing its value, or table.get/set/grow) resolves that dangling TypeIndex — a use-after-free / type-system confusion surfacing as an unexpected process crash. The fix replaces the lone RefPtr<const TypeDefinition> with std::optional<WebAssemblyGCTypeDependencies> m_typeDependencies, populated via m_typeDependencies.emplace(Ref { *typeDefinition }) in each Global/Table constructor, and WebAssemblyGCTypeDependencies retains the definition together with all transitively reachable types. The restored invariant is that a Global or Table that names a GC type keeps alive the entire transitive closure of types its TypeIndex values can reach, not merely the top-level definition.
Attack Path
1. Define nested GC types
Build a wasm module with a rec group where a function type $f takes/returns a struct type $s, i.e. $f transitively references $s.
2. Store the type in a long-lived container
Create an exported global (mut (ref null $f)) or a table with element type (ref null $f) so a WasmGlobal/WasmTable holds a Wasm::Type with $s’s TypeIndex reachable only transitively.
3. Drop the module
Null out the instance/module reference so the only thing pinning $s is (pre-patch) nobody — m_typeDefinition retained just $f.
4. Force collection
Run gc() so the transitively-reachable $s TypeDefinition is freed while the global/table survives.
5. Touch the survivor
Read/write the global value or call table.get/set/grow; resolving the now-dangling TypeIndex dereferences freed memory — crash or type confusion.
Impact Assessment
A controlled use-after-free of a Wasm TypeDefinition that outlives the module which defined its transitive dependencies. Triggering it needs GC timing, and the immediate observable is a crash in the WebContent process, but a reclaimed TypeDefinition slot filled with attacker-influenced data could turn a dangling TypeIndex into type confusion within the wasm type system. Confined to the WebContent sandbox; rated medium (CVSS 6.5).
Audit Directions
Other bare TypeIndex holders
grep wasm/ for TypeInformation::getRef and RefPtr<const TypeDefinition> / RefPtr<const Wasm::TypeDefinition> to find other long-lived objects that pin only the direct definition rather than a WebAssemblyGCTypeDependencies.
Completeness of transitive walk
Audit WebAssemblyGCTypeDependencies to confirm it follows rec-group members, struct/array field types, and func param/result types — any missed edge re-opens the same dangling-TypeIndex class.
Other GC-typed containers
Check tags/exceptions, JS-exposed wrappers (JSWebAssemblyGlobal/Table), and any cache that stores a Wasm::Type past module teardown for the same retention gap.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
Table::Table |
Source/JavaScriptCore/wasm/WasmTable.cpp |
modified | Drops the single m_wasmTypeDefinition RefPtr and instead emplaces a WebAssemblyGCTypeDependencies (m_typeDependencies) that retains the type and all transitively reachable definitions. |
Global::Global (both constructors) |
Source/JavaScriptCore/wasm/WasmGlobal.h |
modified | Same change for globals: replaces m_typeDefinition with a transitive m_typeDependencies so a GC element type keeps its whole type closure alive. |
m_typeDependencies member (Global/Table) |
Source/JavaScriptCore/wasm/WasmGlobal.h |
modified | Field type changed from RefPtr<const TypeDefinition> to std::optional<WebAssemblyGCTypeDependencies>; mirrored in WasmTable.h. |
Files Changed
Source Files
Source/JavaScriptCore/wasm/WasmGlobal.hSource/JavaScriptCore/wasm/WasmTable.cppSource/JavaScriptCore/wasm/WasmTable.h
Test Files
JSTests/wasm/gc/transitive-type-retention-global.jsJSTests/wasm/gc/transitive-type-retention-table.js
Regression Test
The fix ships this proof-of-concept / regression test:
diff --git a/JSTests/wasm/gc/transitive-type-retention-global.js b/JSTests/wasm/gc/transitive-type-retention-global.js
new file mode 100644
index 000000000000..ff5d2608adf8
--- /dev/null
+++ b/JSTests/wasm/gc/transitive-type-retention-global.js
@@ -0,0 +1,24 @@
+// Regression test: Wasm Global must retain transitive TypeDefinition dependencies.
+// A global typed (ref null $f) where $f references struct type $s must keep $s alive
+// even after the module is collected.
+
+import { instantiate } from "./wast-wrapper.js";
+
+(function() {
+ let inst = instantiate(`
+ (module
+ (rec (type $s (struct (field i32) (field i32) (field i32) (field i32))))
+ (rec (type $f (func (param (ref null $s)) (result (ref null $s)))))
+ (global (export "g") (mut (ref null $f)) (ref.null $f))
+ )
+ `);
+
+ let g = inst.exports.g;
+ inst = null;
+
+ gc();
+ gc();
+
+ g.value;
+ g.value = null;
+})();
diff --git a/JSTests/wasm/gc/transitive-type-retention-table.js b/JSTests/wasm/gc/transitive-type-retention-table.js
new file mode 100644
index 000000000000..30886e1a7692
--- /dev/null
+++ b/JSTests/wasm/gc/transitive-type-retention-table.js
@@ -0,0 +1,25 @@
+// Regression test: Wasm Table must retain transitive TypeDefinition dependencies.
+// A table typed (ref null $f) where $f references struct type $s must keep $s alive
+// even after the module is collected.
+
+import { instantiate } from "./wast-wrapper.js";
+
+(function() {
+ let inst = instantiate(`
+ (module
+ (rec (type $s (struct (field i32) (field i32) (field i32) (field i32))))
+ (rec (type $f (func (param (ref null $s)) (result (ref null $s)))))
+ (table (export "tbl") 10 10 (ref null $f))
+ )
+ `);
+
+ let tbl = inst.exports.tbl;
+ inst = null;
+
+ gc();
+ gc();
+
+ tbl.get(0);
+ tbl.set(0, null);
+ tbl.grow(0, null);
+})();