Released June 29, 2026

CVE-2026-43712

CVSS 6.5 severity medium CWE-125CWE-787 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

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.h
  • Source/JavaScriptCore/wasm/WasmTable.cpp
  • Source/JavaScriptCore/wasm/WasmTable.h

Test Files

  • JSTests/wasm/gc/transitive-type-retention-global.js
  • JSTests/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);
+})();