Medium CVSS 4.3 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentJSC Wasm
Bug ClassUAF
Tracker310207
Fix commitc5d68122b2fa (WebKit/WebKit) +55/-9
CWECWE-119, CWE-120 (Buffer bounds error, Buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L
CISA KEVNot listed
CreditedAisle offensive security research team (Joshua Rogers, Luigino Camastra, Igor Morgenstern, and Guido Vranken), Maher Azzouzi, Ngan Nguyen of Calif.io
Disclosed2026-05-11

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

Key insight
The traversal mixed expanded and unexpanded type identities — registering one hash while enqueuing/recursing on the other — so a reachable GC type definition could escape the dependency set and be collected while still live; the fix makes registration and recursion consistently cover both the node and its expansion.

Attack Path

  1. 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.
  2. 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.
  3. Force type-information cleanup / GC Trigger TypeInformation cleanup by creating and discarding another module and calling gc() (as callTypeInformationTryCleanup does), causing the unregistered-but-still-needed TypeDefinition to be reclaimed.
  4. 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.
  5. 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

The patch fixes a dependency-tracking gap that can leave a still-referenced Wasm GC TypeDefinition unregistered and thus eligible for premature reclamation, an object-lifetime (use-after-free-flavored) bug rather than a bounded numeric overflow. The commit establishes an unexpected process crash (LogicError, medium); the diff does not demonstrate a controllable UAF or write primitive, and reaching it requires attacker-defined recursive GC types plus a GC at the right moment. It is confined to the sandboxed WebContent process running the Wasm engine. Escalation to RCE would require demonstrating deterministic reuse of the reclaimed TypeDefinition and heap grooming that the commit does not show.

Changed Functions

FunctionChangeNotes
WebAssemblyGCTypeDependencies::WebAssemblyGCTypeDependencies
Source/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.
appendToWorkIfNeeded
Source/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::process
Source/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 test
JSTests/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.js
  • Source/JavaScriptCore/wasm/WasmTypeDefinition.cpp

Audit Directions

  • Same file: expand()/hash consistency
    In WasmTypeDefinition.cpp grep for expand(), typeHash(, and m_typeDefinitions.add to verify no other traversal records one identity while operating on another, and that every enqueued node is registered before recursion.
  • All isRefWithTypeIndex dependency walks
    Search for isRefWithTypeIndex, TypeInformation::get(, and appendToWorkIfNeeded to find other places that resolve type indices into dependency sets and confirm they register the resolved definition.
  • TypeInformation lifetime/cleanup callers
    Audit 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 handling
    Grep for StructType, ArrayType, FunctionSignature, and sub/recursion-group handling to find traversals that may not follow expand() uniformly for sub-typed or recursive definitions.

Original Bug Report

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