Released December 11, 2024

CVE-2024-54479

CVSS 7.5 severity high vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

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();