CVE-2026-28901
Overview
Background
- Wasm GC type definition
- A structural type (struct, array, or function signature) declared in a WebAssembly module, tracked by JSC’s TypeInformation registry.
- expand()
- Resolves a possibly-projected/sub-typed TypeDefinition to its underlying structural definition; the expanded object may differ in identity from the node it was reached through.
- WebAssemblyGCTypeDependencies
- A helper that walks a type’s reachable definitions and records their hashes in m_typeDefinitions so those definitions stay alive for as long as they are needed.
- TypeInformation cleanup
- A GC-driven pass that reclaims TypeDefinitions no longer referenced; a dependency omitted from m_typeDefinitions can be collected while still in use.
- isRefWithTypeIndex
- Predicate for reference types that carry a concrete type index, i.e. references whose referenced definition must itself be treated as a dependency.
Root Cause Analysis
WebAssemblyGCTypeDependencies collects the set of Wasm GC type definitions a value depends on, so those definitions are kept alive while the dependent object (here a WebAssembly tag/exception referencing a struct-typed reference) exists.
The bug is a lifetime/liveness-tracking error rooted in how the traversal handled expanded vs. unexpanded type definitions. In the vulnerable code the constructor immediately called unexpandedType->expand() and appended the expanded form to the worklist, while only recording typeHash(unexpandedType) in m_typeDefinitions; appendToWorkIfNeeded similarly appended TypeInformation::get(type.index).expand(). process then keyed its visited-set and StructType/ArrayType/FunctionSignature recursion off the raw typeDef it received. The consequence is an inconsistent set of hashes recorded in m_typeDefinitions: the constructor recorded the unexpanded hash but pushed the expanded object, so the dependency set could omit either the unexpanded or the expanded definition that is actually reachable and must be retained. When a type definition is a recursive/sub-typed struct (the regression test builds (sub (struct (field i32))) reached through a func signature parameter (ref $sub1)), the definition that is truly referenced may not be registered as a dependency, so a later TypeInformation cleanup/GC (callTypeInformationTryCleanup runs gc()) can reclaim a TypeDefinition that is still needed.
The patch restores the invariant that every reachable definition — both the node as received and its expansion when they differ — is registered before recursion: the constructor now appends unexpandedType.get() (the unexpanded node) and drops the standalone m_typeDefinitions.add; appendToWorkIfNeeded appends the non-expanded TypeInformation::get(type.index); and process now computes expand() locally, and when the expansion differs from the node it checks-and-adds typeHash(expanded) (with early-out if already visited) before switching all the is<StructType>()/as<...>() recursion to operate on expanded. This guarantees both hashes are tracked and that structural recursion walks the expanded shape, closing the gap that let a live dependency be collected. (Inference: that the missing registration leads to premature reclamation and use-after-free of a TypeDefinition is deduced from the regression harness forcing gc() between tag creation and use plus the LogicError/crash description; the actual free/use sites are in TypeInformation and the exception-matching path, not shown in this diff.)
Attack Path
- Define a recursive GC struct type
Serve a Wasm module (via JS WebAssembly.Module/Instance) declaring a sub-typed struct
(sub (struct (field i32)))and a function signature taking(ref $sub1), exported as a tag, so a reference-with-type-index dependency exists. - Instantiate and capture the tag
Create the instance and hold
instance.exports.tag, which builds a WebAssemblyGCTypeDependencies dependency set that (pre-patch) fails to register the actually-referenced type definition. - Force type-information cleanup / GC
Trigger TypeInformation cleanup by creating and discarding another module and calling gc() (as
callTypeInformationTryCleanupdoes), causing the unregistered-but-still-needed TypeDefinition to be reclaimed. - Reuse the dangling type
Perform an operation that re-consults the freed type definition, e.g.
new WebAssembly.Exception(tag, [{}]), whose argument type-checking dereferences the referenced type. - Observe crash (potential UAF) The dereference of the reclaimed TypeDefinition produces an unexpected process crash; whether it yields a controllable use-after-free is not established by the diff.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
WebAssemblyGCTypeDependencies::WebAssemblyGCTypeDependenciesSource/JavaScriptCore/wasm/WasmTypeDefinition.cpp |
modified | Now seeds the worklist with the unexpanded type node (`unexpandedType.get()`) instead of its `expand()`, and removes the standalone `m_typeDefinitions.add(typeHash(unexpandedType))`, letting `process` do consistent registration. |
appendToWorkIfNeededSource/JavaScriptCore/wasm/WasmTypeDefinition.cpp |
modified | Appends the non-expanded `TypeInformation::get(type.index)` for ref-with-type-index types, so expansion (and its hashing) is handled uniformly inside `process`. |
WebAssemblyGCTypeDependencies::processSource/JavaScriptCore/wasm/WasmTypeDefinition.cpp |
modified | Computes `expand()` locally; when the expansion differs from the node, checks/adds `typeHash(expanded)` (early-out if already present); and drives all StructType/ArrayType/FunctionSignature recursion off `expanded`, ensuring both the node and its expansion are registered as dependencies. |
310207.js regression testJSTests/wasm/regress/310207.js |
added | Reproducer building a sub-typed struct referenced via a tag signature, forcing gc() between tag creation and use, expecting a clean TypeError rather than a crash. |
Files Changed
JSTests/wasm/regress/310207.jsSource/JavaScriptCore/wasm/WasmTypeDefinition.cpp
Audit Directions
- Same file: expand()/hash consistencyIn WasmTypeDefinition.cpp grep for
expand(),typeHash(, andm_typeDefinitions.addto verify no other traversal records one identity while operating on another, and that every enqueued node is registered before recursion. - All isRefWithTypeIndex dependency walksSearch for
isRefWithTypeIndex,TypeInformation::get(, andappendToWorkIfNeededto find other places that resolve type indices into dependency sets and confirm they register the resolved definition. - TypeInformation lifetime/cleanup callersAudit TypeInformation cleanup/GC paths (grep
tryCleanup,RegisteredType, refcount/hash-based liveness) for other consumers that rely on a dependency set being complete, especially around exceptions/tags and GC struct/array types. - Wasm GC recursive/sub type handlingGrep for
StructType,ArrayType,FunctionSignature, andsub/recursion-group handling to find traversals that may not followexpand()uniformly for sub-typed or recursive definitions.