CVE-2026-43712
Overview
Background
- 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
- 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.
- 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. - Drop the module Null out the instance/module reference so the only thing pinning $s is (pre-patch) nobody — m_typeDefinition retained just $f.
- Force collection Run gc() so the transitively-reachable $s TypeDefinition is freed while the global/table survives.
- 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
Files Changed
JSTests/wasm/gc/transitive-type-retention-global.jsJSTests/wasm/gc/transitive-type-retention-table.jsSource/JavaScriptCore/wasm/WasmGlobal.hSource/JavaScriptCore/wasm/WasmTable.cppSource/JavaScriptCore/wasm/WasmTable.h
Audit Directions
- Other bare TypeIndex holdersgrep wasm/ for
TypeInformation::getRefandRefPtr<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 walkAudit 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 containersCheck tags/exceptions, JS-exposed wrappers (JSWebAssemblyGlobal/Table), and any cache that stores a Wasm::Type past module teardown for the same retention gap.
Patch
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;
};
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);
+})();
Regression Test / PoC
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);
+})();