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
Files Changed
Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cppJSTests/wasm/gc/linking.js
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.
Patch
diff --git a/Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp b/Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp
index 53b13a78c1b5..39eb6ab37af4 100644
--- a/Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp
+++ b/Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp
@@ -361,7 +361,7 @@ void WebAssemblyModuleRecord::initializeImports(JSGlobalObject* globalObject, JS
if (!value.inherits<JSWebAssemblyGlobal>())
return exception(createJSWebAssemblyLinkError(globalObject, vm, importFailMessage(import, "imported global"_s, "must be a WebAssembly.Global object since it is mutable"_s)));
JSWebAssemblyGlobal* globalValue = jsCast<JSWebAssemblyGlobal*>(value);
- if (!isSubtype(globalValue->global()->type(), global.type))
+ if (!isSubtype(globalValue->global()->type(), global.type) || !isSubtype(global.type, globalValue->global()->type()))
return exception(createJSWebAssemblyLinkError(globalObject, vm, importFailMessage(import, "imported global"_s, "must be a same type"_s)));
if (globalValue->global()->mutability() != global.mutability)
return exception(createJSWebAssemblyLinkError(globalObject, vm, importFailMessage(import, "imported global"_s, "must be a same mutability"_s)));
@@ -393,7 +393,7 @@ void WebAssemblyModuleRecord::initializeImports(JSGlobalObject* globalObject, JS
auto expectedType = moduleInformation.tables[import.kindIndex].wasmType();
auto actualType = table->table()->wasmType();
- if (!Wasm::isSubtype(actualType, expectedType))
+ if (!Wasm::isSubtype(actualType, expectedType) || !Wasm::isSubtype(expectedType, actualType))
return exception(createJSWebAssemblyLinkError(globalObject, vm, importFailMessage(import, "Table import"_s, "provided a 'type' that is wrong"_s)));
// ii. Append v to tables.
diff --git a/JSTests/wasm/gc/linking.js b/JSTests/wasm/gc/linking.js
index 400aa6143af7..c8b8b82c269b 100644
--- a/JSTests/wasm/gc/linking.js
+++ b/JSTests/wasm/gc/linking.js
@@ -19,19 +19,6 @@ function testLinking() {
`, { m: { f1: m0.exports.f0 } });
}
- {
- const m0 = instantiate(`
- (module
- (type (func))
- (table (export "t0") 10 (ref null 0)))
- `);
-
- const m1 = instantiate(`
- (module
- (table (import "m" "t1") 10 (ref null func)))
- `, { m: { t1: m0.exports.t0 } });
- }
-
{
const m0 = instantiate(`
(module
@@ -44,19 +31,6 @@ function testLinking() {
(global (import "m" "g1") (ref null func)))
`, { m: { g1: m0.exports.g0 } });
}
-
- {
- const m0 = instantiate(`
- (module
- (type (func))
- (global (export "g0") (mut (ref null 0)) (ref.null 0)))
- `);
-
- const m1 = instantiate(`
- (module
- (global (import "m" "g1") (mut (ref null func))))
- `, { m: { g1: m0.exports.g0 } });
- }
}
testLinking();
Regression Test / PoC
diff --git a/JSTests/wasm/gc/linking.js b/JSTests/wasm/gc/linking.js
index 400aa6143af7..c8b8b82c269b 100644
--- a/JSTests/wasm/gc/linking.js
+++ b/JSTests/wasm/gc/linking.js
@@ -19,19 +19,6 @@ function testLinking() {
`, { m: { f1: m0.exports.f0 } });
}
- {
- const m0 = instantiate(`
- (module
- (type (func))
- (table (export "t0") 10 (ref null 0)))
- `);
-
- const m1 = instantiate(`
- (module
- (table (import "m" "t1") 10 (ref null func)))
- `, { m: { t1: m0.exports.t0 } });
- }
-
{
const m0 = instantiate(`
(module
@@ -44,19 +31,6 @@ function testLinking() {
(global (import "m" "g1") (ref null func)))
`, { m: { g1: m0.exports.g0 } });
}
-
- {
- const m0 = instantiate(`
- (module
- (type (func))
- (global (export "g0") (mut (ref null 0)) (ref.null 0)))
- `);
-
- const m1 = instantiate(`
- (module
- (global (import "m" "g1") (mut (ref null func))))
- `, { m: { g1: m0.exports.g0 } });
- }
}
testLinking();