Firefox · SpiderMonkey
CVE-2026-109720
Logic Error in SpiderMonkey
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifjs/src/wasm/WasmIonCompile.cpp |
modified |
Files Changed
js/src/jit-test/tests/wasm/gc/ref-gvn.jsjs/src/wasm/WasmIonCompile.cpp
Patch
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…
References
On This Page