CVE-2024-54479
Overview
Background
- Wasm reference types / subtyping
- Reference types (e.g. (ref null func) vs a specific (ref null $t)) form a subtype lattice used to check import compatibility.
- Covariance vs invariance
- Read-only positions may be covariant (one-way subtype); read-write (mutable) positions must be invariant (both directions) to stay type-safe.
- Mutable global / table
- Wasm storage that can be both read and written through its typed view, shared across importing modules.
- Import linking
- initializeImports binds a module’s imports to provided values, checking their types before use.
Root Cause Analysis
WebAssemblyModuleRecord::initializeImports validates that an imported global or table is type-compatible with the module’s declared import type. For these it used a single one-directional subtype check: for globals isSubtype(globalValue->global()->type(), global.type) and for tables Wasm::isSubtype(actualType, expectedType). Covariant (one-way) subtyping is correct only for immutable/read-only positions; a MUTABLE global and a table are read-write, so their element type must be INVARIANT — compatible in both directions — otherwise a write through one view can store a value of a type the other view considers illegal.
Pre-patch, importing e.g. a table typed (ref null 0) (a specific function type) where (ref null func) is expected (or vice versa) passed the one-way check, so the two modules disagreed on the element type of the same mutable storage — a type confusion in the wasm type system that the engine’s later type-directed accesses rely on.
The fix makes both checks bidirectional: ... || !isSubtype(global.type, globalValue->global()->type()) and ... || !Wasm::isSubtype(expectedType, actualType), i.e. requires mutual subtyping (invariance) for mutable globals and tables, and the removed linking tests are exactly the cases that were wrongly accepted.
The restored invariant is that a shared mutable wasm storage has one agreed-upon element type across importer and exporter.
Attack Path
- Export a mutable global/table Build a wasm module exporting a mutable global or a table with one reference element type.
- Import it under a different type Instantiate another module importing that global/table declared with a merely one-way-compatible (not invariant) reference type.
- Pass the weak check Pre-patch the one-directional isSubtype check accepts the mismatch, linking the two modules.
- Type-confuse via the shared storage Writing through one typed view and reading through the other yields a value of an unexpected reference type — type confusion / memory unsafety in the WebContent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
WebAssemblyModuleRecord::initializeImports (global import)Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp |
modified | Requires mutual subtyping for imported globals: adds `|| !isSubtype(global.type, importedGlobalType)` so a mutable global's type is invariant, not just covariant. |
WebAssemblyModuleRecord::initializeImports (table import)Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp |
modified | Adds the reverse `|| !Wasm::isSubtype(expectedType, actualType)` so an imported table's element type must match invariantly. |
Audit Directions
- Other one-way subtype checksgrep wasm/ for isSubtype(…) guarding mutable/read-write positions (globals, tables, mutable fields) that should require invariance.
- Reference-type linkingAudit table/global/tag import and export matching for covariance used where invariance is required.
- GC struct/array mutable fieldsCheck wasm GC mutable struct/array field subtyping for the same covariance-vs-invariance error.