High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionJIT miscompilation in the JavaScript: WebAssembly component
ComponentSpiderMonkey
Bug ClassLogic Error
Tracker2074160
Fix commit500407a1be99 (firefox) +226/-11
CISA KEVNot listed
CreditedAmy Burnett of OpenAI
Disclosed2026-10-13

Changed Functions

FunctionChangeNotes
if
js/src/wasm/WasmIonCompile.cpp
modified

Files Changed

  • js/src/jit-test/tests/wasm/gc/ref-gvn.js
  • js/src/wasm/WasmIonCompile.cpp
diff --git a/js/src/wasm/WasmIonCompile.cpp b/js/src/wasm/WasmIonCompile.cpp
index 19798bd4bdc..ad131927a7d 100644
--- a/js/src/wasm/WasmIonCompile.cpp
+++ b/js/src/wasm/WasmIonCompile.cpp
@@ -5602,31 +5602,78 @@ class FunctionCompiler {
     MDefinition* ref = values.back();
     MOZ_ASSERT(ref->type() == MIRType::WasmAnyRef);
 
-    MDefinition* success = refTest(ref, destType);
-    if (!success) {
+    // We generally want to emit an MWasmRefTest, an MTest, and an MWasmRefCast
+    // along the test-success branch. The cast is immovable but should be a
+    // no-op 100% of the time because of our test/cast optimizations.
+
+    MDefinition* refTestSuccess = refTest(ref, destType);
+    if (!refTestSuccess) {
       return false;
     }
 
-    MTest* test;
-    if (onSuccess) {
-      test = MTest::New(alloc(), success, nullptr, fallthroughBlock);
+    if (!onSuccess) {
+      // For br_on_cast_fail, the fallthrough path gets the MWasmRefCast
+      // (because the cast succeeded) and we don't need to do anything fancy for
+      // the success path.
+
+      MTest* test =
+          MTest::New(alloc(), refTestSuccess, fallthroughBlock, nullptr);
       if (!test || !addControlFlowPatch(test, labelRelativeDepth,
-                                        MTest::TrueBranchIndex, branchHint)) {
+                                        MTest::FalseBranchIndex, branchHint)) {
         return false;
       }
-    } else {
-      test = MTest::New(alloc(), success, fallthroughBlock, nullptr);
-      if (!test || !addControlFlowPatch(test, labelRelativeDepth,
-                                        MTest::FalseBranchIndex, branchHint)) {
+      if (!pushDefs(values)) {
+        return false;
+      }
+      curBlock_->end(test);
+      curBlock_ = fallthroughBlock;
+
+      MDefinition* cast = refCast(ref, destType);
+      if (!cast) {
         return false;
       }
+      iter().setResult(cast);
+
+      return true;
     }
 
-    if (!pushDefs(values)) {
+    // For br_on_cast, the fallthrough uses the original ref (since the cast
+    // failed), while the success has a new block containing only the
+    // MWasmRefCast.
+
+    MBasicBlock* successBlock = nullptr;
+    if (!newBlock(curBlock_, &successBlock)) {
       return false;
     }
 
+    MTest* test =
+        MTest::New(alloc(), refTestSuccess, successBlock, fallthroughBlock);
+    if (!test) {
+      return false;
+    }
     curBlock_->end(test);
+    curBlock_ = successBlock;
+
+    MDefinition* cast = refCast(ref, destType);
+    if (!cast) {
+      return false;
+    }
+
+    DefVector castValues;
+    if (!castValues.appendAll(values)) {
+      return false;
+    }
+    castValues.back() = cast;
+    if (!pushDefs(castValues)) {
+      return false;
+    }
+
+    MGoto* jump = MGoto::New(alloc());
+    if (!jump || !addControlFlowPatch(jump, labelRelativeDepth,
+                                      MGoto::TargetIndex, branchHint)) {
+      return false;
+    }
+    curBlock_->end(jump);
     curBlock_ = fallthroughBlock;
     return true;
   }
diff --git a/js/src/jit-test/tests/wasm/gc/ref-gvn.js b/js/src/jit-test/tests/wasm/gc/ref-gvn.js
index cca92cff2e8..8ea72dba592 100644
--- a/js/src/jit-test/tests/wasm/gc/ref-gvn.js
+++ b/js/src/jit-test/tests/wasm/gc/ref-gvn.js
@@ -1317,4 +1317,172 @@ if (getBuildConfiguration("jitspew")) {
   }
   // ref.test of (ref none) gets folded into a constant 0 by GVN, so we can't
   // test it the same way.
+
+  // br_on_cast and br_on_cast_fail generate test and cast nodes and get optimized accordingly
+  {
+    const code = wasmTextToBinary(`(module
+      (type $a (sub (struct (field i32))))
+      (type $b (sub $a (struct (field i32) (field i64))))
+
+      (global $g32 (mut i32) (i32.const 0))
+      (global $g64 (mut i64) (i64.const 0))
+
+      (func (export "makeA") (result (ref $a))
+        (struct.new $a (i32.const 123))
+      )
+      (func (export "makeB") (result (ref $b))
+        (struct.new $b (i32.const 123) (i64.const 234))
+      )
+      (func (export "test1") (param $p (ref $a)) (param $n i32) (result i64)
+        (struct.get $a 0 (local.get $p))
+        global.set $g32
+
+        loop
+          block
+            block (result (ref $b))
+              (br_on_cast 0 (ref $a) (ref $b) (local.get $p))
+              (global.set $g64 (i64.const -1))
+              br 1
+            end
+            struct.get $b 1
+            global.set $g64
+          end
+
+          (i32.sub (local.get $n) (i32.const 1))
+          local.tee $n
+          br_if 0
+        end
+
+        global.get $g64
+      )
+      (func (export "test2") (param $p (ref $a)) (param $n i32) (result i64)
+        (struct.get $a 0 (local.get $p))
+        global.set $g32
+
+        loop
+          block
+            block (result (ref $a))
+              (br_on_cast_fail 0 (ref $a) (ref $b) (local.get $p))
+              struct.get $b 1
+              global.set $g64
+              br 1
+            end
+            drop
+            (global.set $g64 (i64.const -1))
+          end
+
+          (i32.sub (local.get $n) (i32.const 1))
+          local.tee $n
+          br_if 0
+        end
+
+        global.get $g64
+      )
+    )`);
+
+    const { makeA, makeB, test1, test2 } = wasmEvalBinary(code).exports;
+    assertEq(test1(makeA(), 10), -1n);
+    assertEq(test2(makeA(), 10), -1n);
+    assertEq(test1(makeB(), 10), 234n);
+    assertEq(test2(makeB(), 10), 234n);
+
+    {
+      const ionJSON = wasmGetIon(code, 2);
+      const unoptimized = wasmIonGetFirstMIRPass(ionJSON);
+      const optimized = wasmIonGetLastMIRPass(ionJSON);
+      assertOpcodesInOrder(unoptimized, ["WasmRefTestConcrete", "Test", "WasmRefCastConcrete", "WasmLoadField"]);
+      assertOpcodesInOrder(optimized, ["WasmRefTestConcrete", "Test", "WasmRefCastInfallible", "WasmLoadField"]);
+    }
+    {
+      const ionJSON = wasmGetIon(code, 3);
+      const unoptimized = wasmIonGetFirstMIRPass(ionJSON);
+      const optimized = wasmIonGetLastMIRPass(ionJSON);
+      assertOpcodesInOrder(unoptimized, ["WasmRefTestConcrete", "Test", "WasmRefCastConcrete", "WasmLoadField"]);
+      assertOpcodesInOrder(optimized, ["WasmRefTestConcrete", "Test", "WasmRefCastInfallible", "WasmLoadField"]);
+    }
+  }
+
+  // same as above but with OOL fields
+  {
+    const code = wasmTextToBinary(`(module
+      (type $a (sub (struct ${"(field i32)".repeat(60)})))
+      (type $b (sub $a (struct ${"(field i32)".repeat(60)} (field i64))))
+
+      (global $g32 (mut i32) (i32.const 0))
+      (global $g64 (mut i64) (i64.const 0))
+
+      (func (export "makeA") (result (ref $a))
+        (struct.new $a ${"(i32.const 123)".repeat(60)})
+      )
+      (func (export "makeB") (result (ref $b))
+        (struct.new $b ${"(i32.const 123)".repeat(60)} (i64.const 234))
+      )
+      (func (export "test1") (param $p (ref $a)) (param $n i32) (result i64)
+        (struct.get $a 59 (local.get $p))
+        global.set $g32
+
+        loop
+          block
+            block (result (ref $b))
+              (br_on_cast 0 (ref $a) (ref $b) (local.get $p))
+              (global.set $g64 (i64.const -1))
+              br 1
+            end
+            struct.get $b 60
+            global.set $g64
+          end
+
+          (i32.sub (local.get $n) (i32.const 1))
+          local.tee $n
+          br_if 0
+        end
+
+        global.get $g64
+      )
+      (func (export "test2") (param $p (ref $a)) (param $n i32) (result i64)
+        (struct.get $a 59 (local.get $p))
+        global.set $g32
+
+        loop
+          block
+            block (result (ref $a))
+              (br_on_cast_fail 0 (ref $a) (ref $b) (local.get $p))
+              struct.get $b 60
+              global.set $g64
+              br 1
+            end
+            drop
+            (global.set $g64 (i64.const -1))
+          end
+
+          (i32.sub (local.get $n) (i32.const 1))
+          local.tee $n
+          br_if 0
+        end
+
+        global.get $g64
+      )
+    )`);
+
+    const { makeA, makeB, test1, test2 } = wasmEvalBinary(code).exports;
+    assertEq(test1(makeA(), 10), -1n);
+    assertEq(test2(makeA(), 10), -1n);
+    assertEq(test1(makeB(), 10), 234n);
+    assertEq(test2(makeB(), 10), 234n);
+
+    {
+      const ionJSON = wasmGetIon(code, 2);
+      const unoptimized = wasmIonGetFirstMIRPass(ionJSON);
+      const optimized = wasmIonGetLastMIRPass(ionJSON);
+      assertOpcodesInOrder(unoptimized, ["WasmRefTestConcrete", "Test", "WasmRefCastConcrete", "WasmLoadField", "WasmLoadField"]);
+      assertOpcodesInOrder(optimized, ["WasmRefTestConcrete", "Test", "WasmRefCastInfallible", "WasmLoadField", "WasmLoadField"]);
+    }
+    {
+      const ionJSON = wasmGetIon(code, 3);
+      const unoptimized = wasmIonGetFirstMIRPass(ionJSON);
+      const optimized = wasmIonGetLastMIRPass(ionJSON);
+      assertOpcodesInOrder(unoptimized, ["WasmRefTestConcrete", "Test", "WasmRefCastConcrete", "WasmLoadField", "WasmLoadField"]);
+      assertOpcodesInOrder(optimized, ["WasmRefTestConcrete", "Test", "WasmRefCastInfallible", "WasmLoadField", "WasmLoadField"]);
+    }
+  }
 }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/js/src/jit-test/tests/wasm/gc/ref-gvn.js b/js/src/jit-test/tests/wasm/gc/ref-gvn.js
index cca92cff2e8..8ea72dba592 100644
--- a/js/src/jit-test/tests/wasm/gc/ref-gvn.js
+++ b/js/src/jit-test/tests/wasm/gc/ref-gvn.js
@@ -1317,4 +1317,172 @@ if (getBuildConfiguration("jitspew")) {
   }
   // ref.test of (ref none) gets folded into a constant 0 by GVN, so we can't
   // test it the same way.
+
+  // br_on_cast and br_on_cast_fail generate test and cast nodes and get optimized accordingly
+  {
+    const code = wasmTextToBinary(`(module
+      (type $a (sub (struct (field i32))))
+      (type $b (sub $a (struct (field i32) (field i64))))
+
+      (global $g32 (mut i32) (i32.const 0))
+      (global $g64 (mut i64) (i64.const 0))
+
+      (func (export "makeA") (result (ref $a))
+        (struct.new $a (i32.const 123))
+      )
+      (func (export "makeB") (result (ref $b))
+        (struct.new $b (i32.const 123) (i64.const 234))
+      )
+      (func (export "test1") (param $p (ref $a)) (param $n i32) (result i64)
+        (struct.get $a 0 (local.get $p))
+        global.set $g32
+
+        loop
+          block
+            block (result (ref $b))
+              (br_on_cast 0 (ref $a) (ref $b) (local.get $p))
+              (global.set $g64 (i64.const -1))
+              br 1
+            end
+            struct.get $b 1
+            global.set $g64
+          end
+
+          (i32.sub (local.get $n) (i32.const 1))
+          local.tee $n
+          br_if 0
+        end
+
+        global.get $g64
+      )
+      (func (export "test2") (param $p (ref $a)) (param $n i32) (result i64)
+        (struct.get $a 0 (local.get $p))
+        global.set $g32
+
+        loop
+          block
+            block (result (ref $a))
+              (br_on_cast_fail 0 (ref $a) (ref $b) (local.get $p))
+              struct.get $b 1
+              global.set $g64
+              br 1
+            end
+            drop
+            (global.set $g64 (i64.const -1))
+          end
+
+          (i32.sub (local.get $n) (i32.const 1))
+          local.tee $n
+          br_if 0
+        end
+
+        global.get $g64
+      )
+    )`);
+
+    const { makeA, makeB, test1, test2 } = wasmEvalBinary(code).exports;
+    assertEq(test1(makeA(), 10), -1n);
+    assertEq(test2(makeA(), 10), -1n);
+    assertEq(test1(makeB(), 10), 234n);
+    assertEq(test2(makeB(), 10), 234n);
+
+    {
+      const ionJSON = wasmGetIon(code, 2);
+      const unoptimized = wasmIonGetFirstMIRPass(ionJSON);
+      const optimized = wasmIonGetLastMIRPass(ionJSON);
+      assertOpcodesInOrder(unoptimized, ["WasmRefTestConcrete", "Test", "WasmRefCastConcrete", "WasmLoadField"]);
+      assertOpcodesInOrder(optimized, ["WasmRefTestConcrete", "Test", "WasmRefCastInfallible", "WasmLoadField"]);
+    }
+    {
+      const ionJSON = wasmGetIon(code, 3);
+      const unoptimized = wasmIonGetFirstMIRPass(ionJSON);
+      const optimized = wasmIonGetLastMIRPass(ionJSON);
+      assertOpcodesInOrder(unoptimized, ["WasmRefTestConcrete", "Test", "WasmRefCastConcrete", "WasmLoadField"]);
+      assertOpcodesInOrder(optimized, ["WasmRefTestConcrete", "Test", "WasmRefCastInfallible", "WasmLoadField"]);
+    }
+  }
+
+  // same as above but with OOL fields
+  {
+    const code = wasmTextToBinary(`(module
+      (type $a (sub (struct ${"(field i32)".repeat(60)})))
+      (type $b (sub $a (struct ${"(field i32)".repeat(60)} (field i64))))
+
+      (global $g32 (mut i32) (i32.const 0))
+      (global $g64 (mut i64) (i64.const 0))
+
+      (func (export "makeA") (result (ref $a))
+        (struct.new $a ${"(i32.const 123)".repeat(60)})
+      )
+      (func (export "makeB") (result (ref $b))
+        (struct.new $b ${"(i32.const 123)".repeat(60)} (i64.const 234))
+      )
+      (func (export "test1") (param $p (ref $a)) (param $n i32) (result i64)
+        (struct.get $a 59 (local.get $p))
+        global.set $g32
+
+        loop
+          block
+            block (result (ref $b))
+              (br_on_cast 0 (ref $a) (ref $b) (local.get $p))
+              (global.set $g64 (i64.const -1))
+              br 1
+            end
+            struct.get $b 60
+            global.set $g64
+          end
+
+          (i32.sub (local.get $n) (i32.const 1))
+          local.tee $n
+          br_if 0
+        end
+
+        global.get $g64
+      )
+      (func (export "test2") (param $p (ref $a)) (param $n i32) (result i64)
+        (struct.get $a 59 (local.get $p))
+        global.set $g32
+
+        loop
+          block
+            block (result (ref $a))
+              (br_on_cast_fail 0 (ref $a) (ref $b) (local.get $p))
+              struct.get $b 60
+              global.set $g64
+              br 1
+            end
+            drop
+            (global.set $g64 (i64.const -1))
+          end
+
+          (i32.sub (local.get $n) (i32.const 1))
+          local.tee $n
+          br_if 0
+        end
+
+        global.get $g64
+      )
+    )`);
+
+    const { makeA, makeB, test1, test2 } = wasmEvalBinary(code).exports;
+    assertEq(test1(makeA(), 10), -1n);
+    assertEq(test2(makeA(), 10), -1n);
+    assertEq(test1(makeB(), 10), 234n);
+    assertEq(test2(makeB(), 10), 234n);
+
+    {
+      const ionJSON = wasmGetIon(code, 2);
+      const unoptimized = wasmIonGetFirstMIRPass(ionJSON);
+      const optimized = wasmIonGetLastMIRPass(ionJSON);
+      assertOpcodesInOrder(unoptimized, ["WasmRefTestConcrete", "Test", "WasmRefCastConcrete", "WasmLoadField", "WasmLoadField"]);
+      assertOpcodesInOrder(optimized, ["WasmRefTestConcrete", "Test", "WasmRefCastInfallible", "WasmLoadField", "WasmLoadField"]);
+    }
+    {
+      const ionJSON = wasmGetIon(code, 3);
+      const unoptimized = wasmIonGetFirstMIRPass(ionJSON);
+      const optimized = wasmIonGetLastMIRPass(ionJSON);
+      assertOpcodesInOrder(unoptimized, ["WasmRefTestConcrete", "Test", "WasmRefCastConcrete", "WasmLoadField", "WasmLoadField"]);
+      assertOpcodesInOrder(optimized, ["WasmRefTestConcrete", "Test", "WasmRefCastInfallible", "WasmLoadField", "WasmLoadField"]);
+    }
+  }
 }
Loading diff…