← All issues

[8] Wasm GC Type Dependency Tracking: Missing Subtype Expansion

Severity: Medium | Component: JSC WebAssembly GC | a0d2715

Rated Medium because the observable effect is missed GC roots for type definitions reachable through Subtype indirection — a logic error that could lead to premature collection and use-after-free of type metadata — but triggering the actual premature collection requires specific GC timing and nested Subtype hierarchies, and the fix is a single-line canonicalization that may have been caught by sanitizers before a reliable exploit was constructed.

Adds a .expand() call in WebAssemblyGCStructureTypeDependencies::process(Wasm::FieldType, WorkList&) so that when a field's type is a Subtype, the expanded (effective) type definition is added to the work list instead of the unexpanded one.

Source/JavaScriptCore/wasm/js/WebAssemblyGCStructure.cpp

void WebAssemblyGCStructureTypeDependencies::process(Wasm::FieldType fieldType, WorkList& work)
{
if (fieldType.type.is<Wasm::Type>()) {
Wasm::Type type = fieldType.type.as<Wasm::Type>();
if (isRefWithTypeIndex(type)) {
- SUPPRESS_UNCHECKED_LOCAL const auto& typeDef = Wasm::TypeInformation::get(type.index);
+ SUPPRESS_UNCHECKED_LOCAL const auto& typeDef = Wasm::TypeInformation::get(type.index).expand();
work.append(typeDef);
}
}
}

Failure to canonicalize Wasm GC subtype references before dependency traversal, causing the type graph walk to miss transitive dependencies reachable only through expanded types.

WebAssembly GC (the gc proposal) introduces struct and array types managed by the JavaScript GC. Types can form subtype hierarchies using the sub keyword — a Subtype wraps a parent type with additional constraints. Internally, WebKit distinguishes between "unexpanded" type definitions (which may be Subtype wrappers) and "expanded" type definitions (the canonical structural form with actual fields). The .expand() method resolves this indirection. WebAssemblyGCStructureTypeDependencies performs a graph traversal of all types reachable from a GC structure's type definition, recording them so the GC knows which type definitions must be kept alive while the structure is live.

Before the fix, the process method retrieved the type definition for a field's type index without calling .expand(). For Subtype fields, the unexpanded type definition is merely a wrapper/alias — the actual structural type with its fields is in the expanded form. By appending the unexpanded form to the work list, the traversal found a type with no fields to recurse into (if unexpanded Subtype definitions lack structural fields, as the fix pattern suggests), and the real expanded type — along with all its transitive dependencies — was never scanned.

The inconsistency is notable: the constructor already called .expand() on the initial type, but the field-level recursion did not. This is a classic pattern where a canonicalization step is applied at one entry point but missed at a recursive call site.

🔒

The ownership and lifetime implications of this GC dependency tracking gap are explored in depth

Subscribe to read more

🔒

Multiple reusable audit patterns identified, with concrete starting points for variant discovery across the Wasm GC type system

Subscribe to read more