CVE-2024-54479
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Seunghyun Lee
Key insight. Mutable wasm globals and tables were type-checked covariantly at import instead of invariantly, letting two modules share the same mutable storage under incompatible reference types — a type confusion; requiring mutual subtyping fixes it.
Patch Details
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();
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.
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2024-54479 |
| Bugzilla | 278497 |
| Component | WebKit |
| Bug Class | TypeConfusion |
| Severity | medium |
| Commit | f95652711d7e02dc… |
| Advisory | Apple Advisory |
Terminology
- 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
1. Export a mutable global/table
Build a wasm module exporting a mutable global or a table with one reference element type.
2. Import it under a different type
Instantiate another module importing that global/table declared with a merely one-way-compatible (not invariant) reference type.
3. Pass the weak check
Pre-patch the one-directional isSubtype check accepts the mismatch, linking the two modules.
4. 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
A wasm type-safety hole (advisory class LogicError, effectively type confusion): a one-directional subtype check let mutable globals/tables link under mismatched reference types, so reads/writes disagree on the element type. That is a memory-unsafety primitive within the wasm type system in the WebContent process, reachable from a crafted pair of modules; the observable is a crash but the class is type confusion. Rated medium.
Audit Directions
Other one-way subtype checks
grep wasm/ for isSubtype(…) guarding mutable/read-write positions (globals, tables, mutable fields) that should require invariance.
Reference-type linking
Audit table/global/tag import and export matching for covariance used where invariance is required.
GC struct/array mutable fields
Check wasm GC mutable struct/array field subtyping for the same covariance-vs-invariance error.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
WebAssemblyModuleRecord::initializeImports (global import) |
Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp |
modified | Requires mutual subtyping for imported globals: adds ` |
WebAssemblyModuleRecord::initializeImports (table import) |
Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp |
modified | Adds the reverse ` |
Regression Test
The fix ships this proof-of-concept / regression test:
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();