Medium CVSS 6.5 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentJSC
Sub-componentJSC Wasm
Bug ClassUAF
Tracker314235
Fix commite96472d9cab9 (WebKit/WebKit) +63/-7
CWECWE-125, CWE-787 (Out-of-bounds read, Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedKwak Kiyong, Song nuri, Tristan Madani (@TristanInSec) from Talence Security
Disclosed2026-06-29

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.

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.

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).

Files Changed

  • JSTests/wasm/gc/transitive-type-retention-global.js
  • JSTests/wasm/gc/transitive-type-retention-table.js
  • Source/JavaScriptCore/wasm/WasmGlobal.h
  • Source/JavaScriptCore/wasm/WasmTable.cpp
  • Source/JavaScriptCore/wasm/WasmTable.h

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.
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);
+})();
Loading diff…

Regression Test / PoC

shipped with the fix
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);
+})();
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.