Medium CVSS 8.8 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to memory corruption
ComponentJSC
Sub-componentJSC Wasm
Bug ClassUAF
Tracker298194
Fix commitd4b9e6993567 (WebKit/WebKit) +2557/-13
CWECWE-787, CWE-119 (Out-of-bounds write, Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle Big Sleep
Disclosed2025-11-03

Background

`WebAssemblyGCStructure`
a Structure subclass that backs WebAssembly GC objects (structs and arrays) and holds references to their Wasm::TypeDefinition and Wasm::RTT.
`Wasm::TypeDefinition::expand()`
resolves a Subtype-wrapped definition to the underlying StructType or ArrayType it describes.
Subtype
a non-final GC type declaration (isFinal = false) that wraps an underlying struct/array type and whose Wasm::Types in field descriptors may reference it by raw pointer.
`TypeInformation::tryCleanup()`
the GC-time reclamation path that frees Wasm::TypeDefinition objects no longer referenced.

Root Cause Analysis

When a JSWebAssemblyInstance builds structures for its GC types, finishCreation called JSWebAssemblyArray::createStructure and JSWebAssemblyStruct::createStructure with typeSignatures[i]->expand(), so the resulting WebAssemblyGCStructure retained only the expanded underlying StructType/ArrayType in m_type. For a Subtype-wrapped declaration, the unexpanded Wasm::TypeDefinition was never kept alive by the structure, yet Wasm::Types inside FieldTypes can hold raw pointers into that unexpanded Subtype. Once no other Ref retained it, TypeInformation::tryCleanup() freed the unexpanded definition during GC, leaving those raw pointers dangling and producing a use-after-free (surfacing as an assertion/corruption during a later gc()).

The fix threads the unexpanded type all the way into WebAssemblyGCStructure, which now stores it in a new Ref<const Wasm::TypeDefinition> m_unexpandedType member, so the wrapping Subtype outlives every raw pointer that targets it. createStructure now takes the unexpanded type and performs expand() internally, keeping both the expanded and unexpanded definitions rooted for the structure’s lifetime.

Key insight
The single mistake was retaining only the expanded StructType/ArrayType (m_type) while raw pointers in field Wasm::Types still targeted the unexpanded Subtype TypeDefinition, allowing GC cleanup to free it; the fix adds an owning Ref (m_unexpandedType) so the Subtype is kept alive as long as the structure exists.

Attack Path

  1. Declare a `Subtype` Craft a module with a non-final struct type B (isFinal = false) so it is represented as a Subtype-wrapped TypeDefinition.
  2. Reference it from a field Declare struct A with a field of wasmRefType(B), embedding a raw pointer to B’s unexpanded Subtype in A’s FieldType.
  3. Instantiate and retain an object Create instances via new_A/new_B and keep one live so its WebAssemblyGCStructure (holding only expanded m_type) persists past instance teardown.
  4. Free the unexpanded type Drop the module/instance and call gc() so TypeInformation::tryCleanup() reclaims the now-unreferenced Subtype definition.
  5. Trigger the use-after-free A further gc() walks the surviving structure/field types, dereferences the dangling raw pointer, and corrupts memory.

Impact Assessment

An attacker running crafted WebAssembly GC content in a page gains a use-after-free on a freed Wasm::TypeDefinition inside the JavaScriptCore/content (renderer) process, enabling memory corruption that can be developed toward arbitrary read/write and code execution. Preconditions are only that the target supports the WebAssembly GC proposal with Subtype (non-final) types and that the attacker can instantiate a module and trigger garbage collection, all reachable from ordinary untrusted web content.

Changed Functions

FunctionChangeNotes
if
JSTests/wasm/spec-harness/wasm-module-builder-gc.js
modified
for
JSTests/wasm/spec-harness/wasm-module-builder-gc.js
modified
RefTypeBuilder
JSTests/wasm/spec-harness/wasm-module-builder-gc.js
modified
constructor
JSTests/wasm/spec-harness/wasm-module-builder-gc.js
modified
nullable
JSTests/wasm/spec-harness/wasm-module-builder-gc.js
modified
shared
JSTests/wasm/spec-harness/wasm-module-builder-gc.js
modified
exact
JSTests/wasm/spec-harness/wasm-module-builder-gc.js
modified

Files Changed

  • JSTests/wasm/regress/298194.js
  • JSTests/wasm/spec-harness/wasm-module-builder-gc.js
  • Source/JavaScriptCore/wasm/js/JSWebAssemblyArrayInlines.h
  • Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
  • Source/JavaScriptCore/wasm/js/JSWebAssemblyStruct.h
  • Source/JavaScriptCore/wasm/js/WebAssemblyGCStructure.cpp
  • Source/JavaScriptCore/wasm/js/WebAssemblyGCStructure.h

Audit Directions

  • Expand-then-drop
    Audit any code that stores TypeDefinition::expand() output without also retaining the original definition, since raw pointers in Wasm::Types/FieldTypes may still target the unexpanded wrapper.
  • Raw pointers across GC
    Review WebAssembly GC structures for raw (non-Ref) references into objects reclaimable by TypeInformation::tryCleanup(), and confirm each has an owning Ref spanning its full lifetime.
  • Subtype lifetime
    Check every path that constructs or copies WebAssemblyGCStructure (including transition/previous-based construction) to ensure both m_type and m_unexpandedType are consistently propagated.
diff --git a/Source/JavaScriptCore/wasm/js/JSWebAssemblyArrayInlines.h b/Source/JavaScriptCore/wasm/js/JSWebAssemblyArrayInlines.h
index ee7f35d7cd94..3cf3d7f38984 100644
--- a/Source/JavaScriptCore/wasm/js/JSWebAssemblyArrayInlines.h
+++ b/Source/JavaScriptCore/wasm/js/JSWebAssemblyArrayInlines.h
@@ -35,17 +35,17 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
 
 namespace JSC {
 
-
 TypeInfoBlob JSWebAssemblyArray::typeInfoBlob()
 {
     return TypeInfoBlob(0, TypeInfo(WebAssemblyGCObjectType, StructureFlags));
 }
 
-WebAssemblyGCStructure* JSWebAssemblyArray::createStructure(VM& vm, JSGlobalObject* globalObject, Ref<const Wasm::TypeDefinition>&& type, Ref<const Wasm::RTT>&& rtt)
+WebAssemblyGCStructure* JSWebAssemblyArray::createStructure(VM& vm, JSGlobalObject* globalObject, Ref<const Wasm::TypeDefinition>&& unexpandedType, Ref<const Wasm::RTT>&& rtt)
 {
+    Ref<const Wasm::TypeDefinition> type { unexpandedType->expand() };
     RELEASE_ASSERT(type->is<Wasm::ArrayType>());
     RELEASE_ASSERT(rtt->kind() == Wasm::RTTKind::Array);
-    return WebAssemblyGCStructure::create(vm, globalObject, TypeInfo(WebAssemblyGCObjectType, StructureFlags), info(), WTFMove(type), WTFMove(rtt));
+    return WebAssemblyGCStructure::create(vm, globalObject, TypeInfo(WebAssemblyGCObjectType, StructureFlags), info(), WTFMove(unexpandedType), WTFMove(type), WTFMove(rtt));
 }
 
 template<typename T>
diff --git a/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp b/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
index 59aee8cdd82d..d8be7f830ec8 100644
--- a/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
+++ b/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
@@ -149,9 +149,9 @@ void JSWebAssemblyInstance::finishCreation(VM& vm)
     for (unsigned i = 0; i < m_moduleInformation->typeCount(); ++i) {
         Ref rtt = m_moduleInformation->rtts[i];
         if (rtt->kind() == RTTKind::Array)
-            gcObjectStructureID(i).set(vm, this, JSWebAssemblyArray::createStructure(vm, globalObject, m_moduleInformation->typeSignatures[i]->expand(), WTFMove(rtt)));
+            gcObjectStructureID(i).set(vm, this, JSWebAssemblyArray::createStructure(vm, globalObject, m_moduleInformation->typeSignatures[i], WTFMove(rtt)));
         else if (rtt->kind() == RTTKind::Struct)
-            gcObjectStructureID(i).set(vm, this, JSWebAssemblyStruct::createStructure(vm, globalObject, m_moduleInformation->typeSignatures[i]->expand(), WTFMove(rtt)));
+            gcObjectStructureID(i).set(vm, this, JSWebAssemblyStruct::createStructure(vm, globalObject, m_moduleInformation->typeSignatures[i], WTFMove(rtt)));
     }
 
     m_vm->traps().registerMirror(m_stackMirror);
diff --git a/Source/JavaScriptCore/wasm/js/JSWebAssemblyStruct.h b/Source/JavaScriptCore/wasm/js/JSWebAssemblyStruct.h
index 67da6cd2ba2e..e621a5ecc516 100644
--- a/Source/JavaScriptCore/wasm/js/JSWebAssemblyStruct.h
+++ b/Source/JavaScriptCore/wasm/js/JSWebAssemblyStruct.h
@@ -85,11 +85,12 @@ TypeInfoBlob JSWebAssemblyStruct::typeInfoBlob()
     return TypeInfoBlob(0, TypeInfo(WebAssemblyGCObjectType, StructureFlags));
 }
 
-WebAssemblyGCStructure* JSWebAssemblyStruct::createStructure(VM& vm, JSGlobalObject* globalObject, Ref<const Wasm::TypeDefinition>&& type, Ref<const Wasm::RTT>&& rtt)
+WebAssemblyGCStructure* JSWebAssemblyStruct::createStructure(VM& vm, JSGlobalObject* globalObject, Ref<const Wasm::TypeDefinition>&& unexpandedType, Ref<const Wasm::RTT>&& rtt)
 {
-    RELEASE_ASSERT(rtt->kind() == Wasm::RTTKind::Struct);
+    Ref<const Wasm::TypeDefinition> type { unexpandedType->expand() };
     RELEASE_ASSERT(type->is<Wasm::StructType>());
-    return WebAssemblyGCStructure::create(vm, globalObject, TypeInfo(WebAssemblyGCObjectType, StructureFlags), info(), WTFMove(type), WTFMove(rtt));
+    RELEASE_ASSERT(rtt->kind() == Wasm::RTTKind::Struct);
+    return WebAssemblyGCStructure::create(vm, globalObject, TypeInfo(WebAssemblyGCObjectType, StructureFlags), info(), WTFMove(unexpandedType), WTFMove(type), WTFMove(rtt));
 }
 
 } // namespace JSC
diff --git a/Source/JavaScriptCore/wasm/js/WebAssemblyGCStructure.cpp b/Source/JavaScriptCore/wasm/js/WebAssemblyGCStructure.cpp
index 3d9188262193..75ba0eec08eb 100644
--- a/Source/JavaScriptCore/wasm/js/WebAssemblyGCStructure.cpp
+++ b/Source/JavaScriptCore/wasm/js/WebAssemblyGCStructure.cpp
@@ -32,10 +32,11 @@
 
 namespace JSC {
 
-WebAssemblyGCStructure::WebAssemblyGCStructure(VM& vm, JSGlobalObject* globalObject, const TypeInfo& typeInfo, const ClassInfo* classInfo, Ref<const Wasm::TypeDefinition>&& type, Ref<const Wasm::RTT>&& rtt)
+WebAssemblyGCStructure::WebAssemblyGCStructure(VM& vm, JSGlobalObject* globalObject, const TypeInfo& typeInfo, const ClassInfo* classInfo, Ref<const Wasm::TypeDefinition>&& unexpandedType, Ref<const Wasm::TypeDefinition>&& type, Ref<const Wasm::RTT>&& rtt)
     : Structure(vm, StructureVariant::WebAssemblyGC, globalObject, typeInfo, classInfo)
     , m_rtt(WTFMove(rtt))
     , m_type(WTFMove(type))
+    , m_unexpandedType(WTFMove(unexpandedType))
 {
     for (unsigned i = 0; i < std::min((m_rtt->displaySizeExcludingThis() + 1), inlinedTypeDisplaySize); ++i)
         m_inlinedTypeDisplay[i] = m_rtt->displayEntry(i);
@@ -46,14 +47,15 @@ WebAssemblyGCStructure::WebAssemblyGCStructure(VM& vm, WebAssemblyGCStructure* p
     , m_rtt(previous->m_rtt)
     , m_type(previous->m_type)
     , m_inlinedTypeDisplay(previous->m_inlinedTypeDisplay)
+    , m_unexpandedType(previous->m_unexpandedType)
 {
 }
 
 
-WebAssemblyGCStructure* WebAssemblyGCStructure::create(VM& vm, JSGlobalObject* globalObject, const TypeInfo& typeInfo, const ClassInfo* classInfo, Ref<const Wasm::TypeDefinition>&& type, Ref<const Wasm::RTT>&& rtt)
+WebAssemblyGCStructure* WebAssemblyGCStructure::create(VM& vm, JSGlobalObject* globalObject, const TypeInfo& typeInfo, const ClassInfo* classInfo, Ref<const Wasm::TypeDefinition>&& unexpandedType, Ref<const Wasm::TypeDefinition>&& type, Ref<const Wasm::RTT>&& rtt)
 {
     ASSERT(vm.structureStructure);
-    WebAssemblyGCStructure* newStructure = new (NotNull, allocateCell<WebAssemblyGCStructure>(vm)) WebAssemblyGCStructure(vm, globalObject, typeInfo, classInfo, WTFMove(type), WTFMove(rtt));
+    WebAssemblyGCStructure* newStructure = new (NotNull, allocateCell<WebAssemblyGCStructure>(vm)) WebAssemblyGCStructure(vm, globalObject, typeInfo, classInfo, WTFMove(unexpandedType), WTFMove(type), WTFMove(rtt));
     newStructure->finishCreation(vm);
     ASSERT(newStructure->type() == StructureType);
     return newStructure;
diff --git a/Source/JavaScriptCore/wasm/js/WebAssemblyGCStructure.h b/Source/JavaScriptCore/wasm/js/WebAssemblyGCStructure.h
index 2dd359097728..879217bd05ff 100644
--- a/Source/JavaScriptCore/wasm/js/WebAssemblyGCStructure.h
+++ b/Source/JavaScriptCore/wasm/js/WebAssemblyGCStructure.h
@@ -56,18 +56,21 @@ public:
     const Wasm::RTT& rtt() const LIFETIME_BOUND { return m_rtt; }
     const Wasm::TypeDefinition& typeDefinition() const LIFETIME_BOUND { return m_type; }
 
-    static WebAssemblyGCStructure* create(VM&, JSGlobalObject*, const TypeInfo&, const ClassInfo*, Ref<const Wasm::TypeDefinition>&&, Ref<const Wasm::RTT>&&);
+    static WebAssemblyGCStructure* create(VM&, JSGlobalObject*, const TypeInfo&, const ClassInfo*, Ref<const Wasm::TypeDefinition>&& unexpandedType, Ref<const Wasm::TypeDefinition>&& expandedType, Ref<const Wasm::RTT>&&);
 
     static constexpr ptrdiff_t offsetOfRTT() { return OBJECT_OFFSETOF(WebAssemblyGCStructure, m_rtt); }
     static constexpr ptrdiff_t offsetOfInlinedTypeDisplay() { return OBJECT_OFFSETOF(WebAssemblyGCStructure, m_inlinedTypeDisplay); }
 
 private:
-    WebAssemblyGCStructure(VM&, JSGlobalObject*, const TypeInfo&, const ClassInfo*, Ref<const Wasm::TypeDefinition>&&, Ref<const Wasm::RTT>&&);
+    WebAssemblyGCStructure(VM&, JSGlobalObject*, const TypeInfo&, const ClassInfo*, Ref<const Wasm::TypeDefinition>&& unexpandedType, Ref<const Wasm::TypeDefinition>&& expandedType, Ref<const Wasm::RTT>&&);
     WebAssemblyGCStructure(VM&, WebAssemblyGCStructure* previous);
 
     Ref<const Wasm::RTT> m_rtt;
     Ref<const Wasm::TypeDefinition> m_type;
     std::array<RefPtr<const Wasm::RTT>, inlinedTypeDisplaySize> m_inlinedTypeDisplay { };
+    // The type of a struct or an array can be a Subtype wrapping the underlying Struct or Array type. In these situations `m_type` retains the expanded underlying type.
+    // We must also retain the original unexpanded Subtype because it may be the target of raw pointers in Wasm::Types of FieldTypes.
+    Ref<const Wasm::TypeDefinition> m_unexpandedType;
 };
 
 } // namespace JSC
diff --git a/JSTests/wasm/regress/298194.js b/JSTests/wasm/regress/298194.js
new file mode 100644
index 000000000000..7707bb7ce25f
--- /dev/null
+++ b/JSTests/wasm/regress/298194.js
@@ -0,0 +1,46 @@
+load("../spec-harness/wasm-module-builder-gc.js");
+
+function createWasmModule() {
+    let builder = new WasmModuleBuilder();
+
+    // Use a Subtype for B by setting isFinal = false.
+    const isFinal = false;
+    const supertypeIndex = kNoSuperType;
+
+    const typeBIdx = builder.addStruct([], supertypeIndex, isFinal);
+    const typeAIdx = builder.addStruct([makeField(wasmRefType(typeBIdx), true)]);
+
+    const refA = wasmRefType(typeAIdx);
+    const refB = wasmRefType(typeBIdx);
+
+    const sigNewA = builder.addType(makeSig([refB], [refA]));
+    builder.addFunction("new_A", sigNewA)
+        .addBody([
+          kExprLocalGet, 0,
+          kGCPrefix, kExprStructNew, ...wasmUnsignedLeb(typeAIdx)
+        ]).exportFunc();
+    const sigNewB = builder.addType(makeSig([], [refB]));
+    builder.addFunction("new_B", sigNewB)
+        .addBody([ kGCPrefix, kExprStructNewDefault, ...wasmUnsignedLeb(typeBIdx) ]).exportFunc();
+
+    return builder.instantiate();
+}
+
+let obj = null;
+{
+    let inst = createWasmModule();
+    let exports = inst.exports;
+    let objB = exports.new_B();
+    obj = exports.new_A(objB);
+}
+
+gc();
+
+let dummy = (new WasmModuleBuilder()).instantiate();
+dummy = null;
+
+// Trigger TypeInformation::tryCleanup() to free the TypeDefinition.
+gc();
+
+// Trigger assertion failure during GC.
+gc();
\ No newline at end of file
diff --git a/JSTests/wasm/spec-harness/wasm-module-builder-gc.js b/JSTests/wasm/spec-harness/wasm-module-builder-gc.js
new file mode 100644
index 000000000000..5c96cbba7980
--- /dev/null
+++ b/JSTests/wasm/spec-harness/wasm-module-builder-gc.js
@@ -0,0 +1,2492 @@
+// Copyright 2016 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This is a 'wasm-module-builder.js' circa 2025 with support for the GC proposal,
+// in particular Subtype-wrapped types.
+
+// Used for encoding f32 and double constants to bits.
+let byte_view = new Uint8Array(8);
+let data_view = new DataView(byte_view.buffer);
+
+// The bytes function receives one of
+//  - several arguments, each of which is either a number or a string of length
+//    1; if it's a string, the charcode of the contained character is used.
+//  - a single array argument containing the actual arguments
+//  - a single string; the returned buffer will contain the char codes of all
+//    contained characters.
+function bytes(...input) {
+  if (input.length == 1 && typeof input[0] == 'array') input = input[0];
+  if (input.length == 1 && typeof input[0] == 'string') {
+    let len = input[0].length;
+    let view = new Uint8Array(len);
+    for (let i = 0; i < len; i++) view[i] = input[0].charCodeAt(i);
+    return view.buffer;
+  }
+  let view = new Uint8Array(input.length);
+  for (let i = 0; i < input.length; i++) {
+    let val = input[i];
+    if (typeof val == 'string') {
+      if (val.length != 1) {
+        throw new Error('string inputs must have length 1');
+      }
+      val = val.charCodeAt(0);
+    }
+    view[i] = val | 0;
+  }
+  return view.buffer;
+}
+
+// Header declaration constants
+var kWasmH0 = 0;
+var kWasmH1 = 0x61;
+var kWasmH2 = 0x73;
+var kWasmH3 = 0x6d;
+
+var kWasmV0 = 0x1;
+var kWasmV1 = 0;
+var kWasmV2 = 0;
+var kWasmV3 = 0;
+
+var kHeaderSize = 8;
+var kPageSize = 65536;
+var kSpecMaxPages = 65536;
+var kMaxVarInt32Size = 5;
+var kMaxVarInt64Size = 10;
+var kSpecMaxFunctionParams = 1_000;
+
+let kDeclNoLocals = 0;
+
+// Section declaration constants
+let kUnknownSectionCode = 0;
+let kTypeSectionCode = 1;        // Function signature declarations
+let kImportSectionCode = 2;      // Import declarations
+let kFunctionSectionCode = 3;    // Function declarations
+let kTableSectionCode = 4;       // Indirect function table and other tables
+let kMemorySectionCode = 5;      // Memory attributes
+let kGlobalSectionCode = 6;      // Global declarations
+let kExportSectionCode = 7;      // Exports
+let kStartSectionCode = 8;       // Start function declaration
+let kElementSectionCode = 9;     // Elements section
+let kCodeSectionCode = 10;       // Function code
+let kDataSectionCode = 11;       // Data segments
+let kDataCountSectionCode = 12;  // Data segment count (between Element & Code)
+let kTagSectionCode = 13;        // Tag section (between Memory & Global)
+let kStringRefSectionCode = 14;  // Stringref literals section (between Tag & Global)
+let kLastKnownSectionCode = 14;
+
+// Name section types
+let kModuleNameCode = 0;
+let kFunctionNamesCode = 1;
+let kLocalNamesCode = 2;
+
+let kWasmSharedTypeForm = 0x65;
+let kWasmFunctionTypeForm = 0x60;
+let kWasmStructTypeForm = 0x5f;
+let kWasmArrayTypeForm = 0x5e;
+let kWasmSubtypeForm = 0x50;
+let kWasmSubtypeFinalForm = 0x4f;
+let kWasmRecursiveTypeGroupForm = 0x4e;
+let kWasmDescriptorTypeForm = 0x4d;
+let kWasmDescribesTypeForm = 0x4c;
+
+let kNoSuperType = 0xFFFFFFFF;
+
+let kLimitsNoMaximum = 0x00;
+let kLimitsWithMaximum = 0x01;
+let kLimitsSharedNoMaximum = 0x02;
+let kLimitsSharedWithMaximum = 0x03;
+let kLimitsMemory64NoMaximum = 0x04;
+let kLimitsMemory64WithMaximum = 0x05;
+let kLimitsMemory64SharedNoMaximum = 0x06;
+let kLimitsMemory64SharedWithMaximum = 0x07;
+
+// Segment flags
+let kActiveNoIndex = 0;
+let kPassive = 1;
+let kActiveWithIndex = 2;
+let kDeclarative = 3;
+let kPassiveWithElements = 5;
+let kDeclarativeWithElements = 7;
+
+// Function declaration flags
+let kDeclFunctionName = 0x01;
+let kDeclFunctionImport = 0x02;
+let kDeclFunctionLocals = 0x04;
+let kDeclFunctionExport = 0x08;
+
+// Value types and related
+let kWasmVoid = 0x40;
+let kWasmI32 = 0x7f;
+let kWasmI64 = 0x7e;
+let kWasmF32 = 0x7d;
+let kWasmF64 = 0x7c;
+let kWasmS128 = 0x7b;
+let kWasmI8 = 0x78;
+let kWasmI16 = 0x77;
+let kWasmF16 = 0x76;
+
+// These are defined as negative integers to distinguish them from positive type
+// indices.
+let kWasmNullFuncRef = -0x0d;
+let kWasmNullExternRef = -0x0e;
+let kWasmNullRef = -0x0f;
+let kWasmFuncRef = -0x10;
+let kWasmAnyFunc = kWasmFuncRef;  // Alias named as in the JS API spec
+let kWasmExternRef = -0x11;
+let kWasmAnyRef = -0x12;
+let kWasmEqRef = -0x13;
+let kWasmI31Ref = -0x14;
+let kWasmStructRef = -0x15;
+let kWasmArrayRef = -0x16;
+let kWasmExnRef = -0x17;
+let kWasmNullExnRef = -0x0c;
+let kWasmStringRef = -0x19;
+let kWasmStringViewWtf8 = -0x1a;
+let kWasmStringViewWtf16 = -0x20;
+let kWasmStringViewIter = -0x1f;
+const kWasmContRef = -0x18;
+const kWasmNullContRef = -0x0b;
+
+// Use the positive-byte versions inside function bodies.
+let kLeb128Mask = 0x7f;
+let kFuncRefCode = kWasmFuncRef & kLeb128Mask;
+let kAnyFuncCode = kFuncRefCode;  // Alias named as in the JS API spec
+let kExternRefCode = kWasmExternRef & kLeb128Mask;
+let kAnyRefCode = kWasmAnyRef & kLeb128Mask;
+let kEqRefCode = kWasmEqRef & kLeb128Mask;
+let kI31RefCode = kWasmI31Ref & kLeb128Mask;
+let kNullExternRefCode = kWasmNullExternRef & kLeb128Mask;
+let kNullFuncRefCode = kWasmNullFuncRef & kLeb128Mask;
+let kStructRefCode = kWasmStructRef & kLeb128Mask;
+let kArrayRefCode = kWasmArrayRef & kLeb128Mask;
+let kExnRefCode = kWasmExnRef & kLeb128Mask;
+let kNullExnRefCode = kWasmNullExnRef & kLeb128Mask;
+let kNullRefCode = kWasmNullRef & kLeb128Mask;
+let kStringRefCode = kWasmStringRef & kLeb128Mask;
+let kStringViewWtf8Code = kWasmStringViewWtf8 & kLeb128Mask;
+let kStringViewWtf16Code = kWasmStringViewWtf16 & kLeb128Mask;
+let kStringViewIterCode = kWasmStringViewIter & kLeb128Mask;
+const kContRefCode = kWasmContRef & kLeb128Mask;
+const kNullContRefCode = kWasmNullContRef & kLeb128Mask;
+
+let kWasmRefNull = 0x63;
+let kWasmRef = 0x64;
+let kWasmExact = 0x62;
+
+// Implementation detail of `wasmRef[Null]Type`, don't use directly.
+class RefTypeBuilder {
+  constructor(opcode, heap_type) {
+    this.opcode = opcode;
+    this.heap_type = heap_type;
+    this.is_shared = false;
+    this.is_exact = false;
+  }
+  nullable() {
+    this.opcode = kWasmRefNull;
+    return this;
+  }
+  shared() {
+    this.is_shared = true;
+    return this;
+  }
+  exact() {
+    this.is_exact = true;
+    return this;
+  }
+}
+function wasmRefNullType(heap_type) {
+  return new RefTypeBuilder(kWasmRefNull, heap_type);
+}
+function wasmRefType(heap_type) {
+  return new RefTypeBuilder(kWasmRef, heap_type);
+}
+
+let kExternalFunction = 0;
+let kExternalTable = 1;
+let kExternalMemory = 2;
+let kExternalGlobal = 3;
+let kExternalTag = 4;
+
+let kTableZero = 0;
+let kMemoryZero = 0;
+let kSegmentZero = 0;
+
+let kExceptionAttribute = 0;
+
+const kAtomicSeqCst = 0;
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/JSTests/wasm/regress/298194.js b/JSTests/wasm/regress/298194.js
new file mode 100644
index 000000000000..7707bb7ce25f
--- /dev/null
+++ b/JSTests/wasm/regress/298194.js
@@ -0,0 +1,46 @@
+load("../spec-harness/wasm-module-builder-gc.js");
+
+function createWasmModule() {
+    let builder = new WasmModuleBuilder();
+
+    // Use a Subtype for B by setting isFinal = false.
+    const isFinal = false;
+    const supertypeIndex = kNoSuperType;
+
+    const typeBIdx = builder.addStruct([], supertypeIndex, isFinal);
+    const typeAIdx = builder.addStruct([makeField(wasmRefType(typeBIdx), true)]);
+
+    const refA = wasmRefType(typeAIdx);
+    const refB = wasmRefType(typeBIdx);
+
+    const sigNewA = builder.addType(makeSig([refB], [refA]));
+    builder.addFunction("new_A", sigNewA)
+        .addBody([
+          kExprLocalGet, 0,
+          kGCPrefix, kExprStructNew, ...wasmUnsignedLeb(typeAIdx)
+        ]).exportFunc();
+    const sigNewB = builder.addType(makeSig([], [refB]));
+    builder.addFunction("new_B", sigNewB)
+        .addBody([ kGCPrefix, kExprStructNewDefault, ...wasmUnsignedLeb(typeBIdx) ]).exportFunc();
+
+    return builder.instantiate();
+}
+
+let obj = null;
+{
+    let inst = createWasmModule();
+    let exports = inst.exports;
+    let objB = exports.new_B();
+    obj = exports.new_A(objB);
+}
+
+gc();
+
+let dummy = (new WasmModuleBuilder()).instantiate();
+dummy = null;
+
+// Trigger TypeInformation::tryCleanup() to free the TypeDefinition.
+gc();
+
+// Trigger assertion failure during GC.
+gc();
\ No newline at end of file
diff --git a/JSTests/wasm/spec-harness/wasm-module-builder-gc.js b/JSTests/wasm/spec-harness/wasm-module-builder-gc.js
new file mode 100644
index 000000000000..5c96cbba7980
--- /dev/null
+++ b/JSTests/wasm/spec-harness/wasm-module-builder-gc.js
@@ -0,0 +1,2492 @@
+// Copyright 2016 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This is a 'wasm-module-builder.js' circa 2025 with support for the GC proposal,
+// in particular Subtype-wrapped types.
+
+// Used for encoding f32 and double constants to bits.
+let byte_view = new Uint8Array(8);
+let data_view = new DataView(byte_view.buffer);
+
+// The bytes function receives one of
+//  - several arguments, each of which is either a number or a string of length
+//    1; if it's a string, the charcode of the contained character is used.
+//  - a single array argument containing the actual arguments
+//  - a single string; the returned buffer will contain the char codes of all
+//    contained characters.
+function bytes(...input) {
+  if (input.length == 1 && typeof input[0] == 'array') input = input[0];
+  if (input.length == 1 && typeof input[0] == 'string') {
+    let len = input[0].length;
+    let view = new Uint8Array(len);
+    for (let i = 0; i < len; i++) view[i] = input[0].charCodeAt(i);
+    return view.buffer;
+  }
+  let view = new Uint8Array(input.length);
+  for (let i = 0; i < input.length; i++) {
+    let val = input[i];
+    if (typeof val == 'string') {
+      if (val.length != 1) {
+        throw new Error('string inputs must have length 1');
+      }
+      val = val.charCodeAt(0);
+    }
+    view[i] = val | 0;
+  }
+  return view.buffer;
+}
+
+// Header declaration constants
+var kWasmH0 = 0;
+var kWasmH1 = 0x61;
+var kWasmH2 = 0x73;
+var kWasmH3 = 0x6d;
+
+var kWasmV0 = 0x1;
+var kWasmV1 = 0;
+var kWasmV2 = 0;
+var kWasmV3 = 0;
+
+var kHeaderSize = 8;
+var kPageSize = 65536;
+var kSpecMaxPages = 65536;
+var kMaxVarInt32Size = 5;
+var kMaxVarInt64Size = 10;
+var kSpecMaxFunctionParams = 1_000;
+
+let kDeclNoLocals = 0;
+
+// Section declaration constants
+let kUnknownSectionCode = 0;
+let kTypeSectionCode = 1;        // Function signature declarations
+let kImportSectionCode = 2;      // Import declarations
+let kFunctionSectionCode = 3;    // Function declarations
+let kTableSectionCode = 4;       // Indirect function table and other tables
+let kMemorySectionCode = 5;      // Memory attributes
+let kGlobalSectionCode = 6;      // Global declarations
+let kExportSectionCode = 7;      // Exports
+let kStartSectionCode = 8;       // Start function declaration
+let kElementSectionCode = 9;     // Elements section
+let kCodeSectionCode = 10;       // Function code
+let kDataSectionCode = 11;       // Data segments
+let kDataCountSectionCode = 12;  // Data segment count (between Element & Code)
+let kTagSectionCode = 13;        // Tag section (between Memory & Global)
+let kStringRefSectionCode = 14;  // Stringref literals section (between Tag & Global)
+let kLastKnownSectionCode = 14;
+
+// Name section types
+let kModuleNameCode = 0;
+let kFunctionNamesCode = 1;
+let kLocalNamesCode = 2;
+
+let kWasmSharedTypeForm = 0x65;
+let kWasmFunctionTypeForm = 0x60;
+let kWasmStructTypeForm = 0x5f;
+let kWasmArrayTypeForm = 0x5e;
+let kWasmSubtypeForm = 0x50;
+let kWasmSubtypeFinalForm = 0x4f;
+let kWasmRecursiveTypeGroupForm = 0x4e;
+let kWasmDescriptorTypeForm = 0x4d;
+let kWasmDescribesTypeForm = 0x4c;
+
+let kNoSuperType = 0xFFFFFFFF;
+
+let kLimitsNoMaximum = 0x00;
+let kLimitsWithMaximum = 0x01;
+let kLimitsSharedNoMaximum = 0x02;
+let kLimitsSharedWithMaximum = 0x03;
+let kLimitsMemory64NoMaximum = 0x04;
+let kLimitsMemory64WithMaximum = 0x05;
+let kLimitsMemory64SharedNoMaximum = 0x06;
+let kLimitsMemory64SharedWithMaximum = 0x07;
+
+// Segment flags
+let kActiveNoIndex = 0;
+let kPassive = 1;
+let kActiveWithIndex = 2;
+let kDeclarative = 3;
+let kPassiveWithElements = 5;
+let kDeclarativeWithElements = 7;
+
+// Function declaration flags
+let kDeclFunctionName = 0x01;
+let kDeclFunctionImport = 0x02;
+let kDeclFunctionLocals = 0x04;
+let kDeclFunctionExport = 0x08;
+
+// Value types and related
+let kWasmVoid = 0x40;
+let kWasmI32 = 0x7f;
+let kWasmI64 = 0x7e;
+let kWasmF32 = 0x7d;
+let kWasmF64 = 0x7c;
+let kWasmS128 = 0x7b;
+let kWasmI8 = 0x78;
+let kWasmI16 = 0x77;
+let kWasmF16 = 0x76;
+
+// These are defined as negative integers to distinguish them from positive type
+// indices.
+let kWasmNullFuncRef = -0x0d;
+let kWasmNullExternRef = -0x0e;
+let kWasmNullRef = -0x0f;
+let kWasmFuncRef = -0x10;
+let kWasmAnyFunc = kWasmFuncRef;  // Alias named as in the JS API spec
+let kWasmExternRef = -0x11;
+let kWasmAnyRef = -0x12;
+let kWasmEqRef = -0x13;
+let kWasmI31Ref = -0x14;
+let kWasmStructRef = -0x15;
+let kWasmArrayRef = -0x16;
+let kWasmExnRef = -0x17;
+let kWasmNullExnRef = -0x0c;
+let kWasmStringRef = -0x19;
+let kWasmStringViewWtf8 = -0x1a;
+let kWasmStringViewWtf16 = -0x20;
+let kWasmStringViewIter = -0x1f;
+const kWasmContRef = -0x18;
+const kWasmNullContRef = -0x0b;
+
+// Use the positive-byte versions inside function bodies.
+let kLeb128Mask = 0x7f;
+let kFuncRefCode = kWasmFuncRef & kLeb128Mask;
+let kAnyFuncCode = kFuncRefCode;  // Alias named as in the JS API spec
+let kExternRefCode = kWasmExternRef & kLeb128Mask;
+let kAnyRefCode = kWasmAnyRef & kLeb128Mask;
+let kEqRefCode = kWasmEqRef & kLeb128Mask;
+let kI31RefCode = kWasmI31Ref & kLeb128Mask;
+let kNullExternRefCode = kWasmNullExternRef & kLeb128Mask;
+let kNullFuncRefCode = kWasmNullFuncRef & kLeb128Mask;
+let kStructRefCode = kWasmStructRef & kLeb128Mask;
+let kArrayRefCode = kWasmArrayRef & kLeb128Mask;
+let kExnRefCode = kWasmExnRef & kLeb128Mask;
+let kNullExnRefCode = kWasmNullExnRef & kLeb128Mask;
+let kNullRefCode = kWasmNullRef & kLeb128Mask;
+let kStringRefCode = kWasmStringRef & kLeb128Mask;
+let kStringViewWtf8Code = kWasmStringViewWtf8 & kLeb128Mask;
+let kStringViewWtf16Code = kWasmStringViewWtf16 & kLeb128Mask;
+let kStringViewIterCode = kWasmStringViewIter & kLeb128Mask;
+const kContRefCode = kWasmContRef & kLeb128Mask;
+const kNullContRefCode = kWasmNullContRef & kLeb128Mask;
+
+let kWasmRefNull = 0x63;
+let kWasmRef = 0x64;
+let kWasmExact = 0x62;
+
+// Implementation detail of `wasmRef[Null]Type`, don't use directly.
+class RefTypeBuilder {
+  constructor(opcode, heap_type) {
+    this.opcode = opcode;
+    this.heap_type = heap_type;
+    this.is_shared = false;
+    this.is_exact = false;
+  }
+  nullable() {
+    this.opcode = kWasmRefNull;
+    return this;
+  }
+  shared() {
+    this.is_shared = true;
+    return this;
+  }
+  exact() {
+    this.is_exact = true;
+    return this;
+  }
+}
+function wasmRefNullType(heap_type) {
+  return new RefTypeBuilder(kWasmRefNull, heap_type);
+}
+function wasmRefType(heap_type) {
+  return new RefTypeBuilder(kWasmRef, heap_type);
+}
+
+let kExternalFunction = 0;
+let kExternalTable = 1;
+let kExternalMemory = 2;
+let kExternalGlobal = 3;
+let kExternalTag = 4;
+
+let kTableZero = 0;
+let kMemoryZero = 0;
+let kSegmentZero = 0;
+
+let kExceptionAttribute = 0;
+
+const kAtomicSeqCst = 0;
+const kAtomicAcqRel = 1;
+
+// Useful signatures
+let kSig_i_i = makeSig([kWasmI32], [kWasmI32]);
+let kSig_l_l = makeSig([kWasmI64], [kWasmI64]);
+let kSig_i_l = makeSig([kWasmI64], [kWasmI32]);
+let kSig_i_ii = makeSig([kWasmI32, kWasmI32], [kWasmI32]);
+let kSig_i_iii = makeSig([kWasmI32, kWasmI32, kWasmI32], [kWasmI32]);
+let kSig_i_iiii = makeSig([kWasmI32, kWasmI32, kWasmI32, kWasmI32], [kWasmI32]);
+let kSig_v_iiii = makeSig([kWasmI32, kWasmI32, kWasmI32, kWasmI32], []);
+let kSig_l_iiii = makeSig([kWasmI32, kWasmI32, kWasmI32, kWasmI32], [kWasmI64]);
+let kSig_l_i = makeSig([kWasmI32], [kWasmI64]);
+let kSig_f_i = makeSig([kWasmI32], [kWasmF32]);
+let kSig_i_f = makeSig([kWasmF32], [kWasmI32]);
+let kSig_i_ff = makeSig([kWasmF32, kWasmF32], [kWasmI32]);
+let kSig_f_ff = makeSig([kWasmF32, kWasmF32], [kWasmF32]);
+let kSig_f_ffff = makeSig([kWasmF32, kWasmF32, kWasmF32, kWasmF32], [kWasmF32]);
+let kSig_d_dd = makeSig([kWasmF64, kWasmF64], [kWasmF64]);
+let kSig_d_dddd = makeSig([kWasmF64, kWasmF64, kWasmF64, kWasmF64], [kWasmF64]);
+let kSig_l_ll = makeSig([kWasmI64, kWasmI64], [kWasmI64]);
+let kSig_l_llll = makeSig([kWasmI64, kWasmI64, kWasmI64, kWasmI64], [kWasmI64]);
+let kSig_i_dd = makeSig([kWasmF64, kWasmF64], [kWasmI32]);
+let kSig_v_v = makeSig([], []);
+let kSig_i_v = makeSig([], [kWasmI32]);
... (truncated)
Loading diff…

Original Bug Report

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