High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionUse-after-free in the JavaScript Engine: JIT component
ComponentSpiderMonkey
Bug ClassLogic Error
Tracker2078837
Fix commit60da826357fb (firefox) +62/-16
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Changed Functions

FunctionChangeNotes
if
js/src/jit/CodeGenerator.cpp
modified
CallNativeExitFrameLayout
js/src/jit/JitFrames.h
modified
if
js/src/vm/PortableBaselineInterpret.cpp
modified
for
js/src/jit-test/tests/ion/bug2078837.js
modified
while
js/src/jit-test/tests/ion/bug2078837.js
modified

Files Changed

  • js/src/builtin/TestingFunctions.cpp
  • js/src/jit-test/tests/ion/bug2078837.js
  • js/src/jit/BaselineCacheIRCompiler.cpp
  • js/src/jit/CodeGenerator.cpp
  • js/src/jit/IonCacheIRCompiler.cpp
  • js/src/jit/JitFrames.cpp
  • js/src/jit/JitFrames.h
  • js/src/vm/PortableBaselineInterpret.cpp
diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp
index 163f580fc13..d69dddd6f1f 100644
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -3647,6 +3647,13 @@ static bool CaptureFirstSubsumedFrame(JSContext* cx, unsigned argc,
   return true;
 }
 
+static bool CallAfterSettingRval(JSContext* cx, unsigned argc, Value* vp) {
+  CallArgs args = CallArgsFromVp(argc, vp);
+  args.rval().setUndefined();
+  return JS::Call(cx, UndefinedHandleValue, args.get(0),
+                  JS::HandleValueArray::empty(), args.rval());
+}
+
 static bool CallFunctionFromNativeFrame(JSContext* cx, unsigned argc,
                                         Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
@@ -10542,6 +10549,11 @@ static const JSFunctionSpecWithHelp TestingFunctions[] = {
 "  object's compartment's principals. If 'shouldIgnoreSelfHosted' is given,\n"
 "  control whether self-hosted frames are considered when checking principals."),
 
+    JS_FN_HELP("callAfterSettingRval", CallAfterSettingRval, 1, 0,
+"callAfterSettingRval(callback)",
+"  Set the return value to undefined, then call callback with no arguments\n"
+"  and return its result."),
+
     JS_FN_HELP("callFunctionFromNativeFrame", CallFunctionFromNativeFrame, 1, 0,
 "callFunctionFromNativeFrame(function)",
 "  Call 'function' with a (C++-)native frame on stack.\n"
diff --git a/js/src/jit/BaselineCacheIRCompiler.cpp b/js/src/jit/BaselineCacheIRCompiler.cpp
index 47651df52df..bd11ae2028d 100644
--- a/js/src/jit/BaselineCacheIRCompiler.cpp
+++ b/js/src/jit/BaselineCacheIRCompiler.cpp
@@ -2985,6 +2985,7 @@ bool BaselineCacheIRCompiler::emitCallNativeShared(
   masm.moveStackPtrTo(scratch2.get());
 
   // Construct a native exit frame.
+  masm.push(calleeReg);
   masm.push(argcReg);
 
   masm.push(FrameDescriptor(FrameType::BaselineStub));
diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
index edb5b891922..26d71433062 100644
--- a/js/src/jit/CodeGenerator.cpp
+++ b/js/src/jit/CodeGenerator.cpp
@@ -6280,6 +6280,13 @@ void CodeGenerator::emitCallNative(LCallIns* call, JSNative native,
   masm.loadJSContext(argContextReg);
   masm.moveStackPtrTo(argVpReg);
 
+  // Initialize |NativeExitFrameLayout::savedCallee_|.
+  if constexpr (std::is_same_v<LCallIns, LCallClassHook>) {
+    masm.Push(ToRegister(call->getCallee()));
+  } else {
+    masm.Push(ImmGCPtr(call->mir()->getSingleTarget()->rawNativeJSFunction()));
+  }
+
   // Initialize |NativeExitFrameLayout::argc_|.
   masm.Push(argUintNReg);
 
@@ -6449,6 +6456,11 @@ void CodeGenerator::visitCallDOMNative(LCallDOMNative* call) {
   MOZ_ASSERT(target->hasJitInfo());
   MOZ_ASSERT(call->mir()->isCallDOMNative());
 
+  // CanAttachDOMCall only optimizes DOM calls for same-realm targets. This
+  // means that IonDOMMethod native exit frames don't need a copy of the callee.
+  // See NativeExitFrameLayout::savedCallee_.
+  MOZ_RELEASE_ASSERT(!call->mir()->maybeCrossRealm());
+
   int unusedStack = UnusedStackBytesForCall(call->mir()->paddedNumStackArgs());
 
   // Registers used for callWithABI() argument-passing.
@@ -6507,12 +6519,6 @@ void CodeGenerator::visitCallDOMNative(LCallDOMNative* call) {
   masm.Push(argObj);
   masm.moveStackPtrTo(argObj);
 
-  if (call->mir()->maybeCrossRealm()) {
-    // We use argJSContext as scratch register here.
-    masm.movePtr(ImmGCPtr(target->rawNativeJSFunction()), argJSContext);
-    masm.switchToObjectRealm(argJSContext, argJSContext);
-  }
-
   bool preTenureWrapperAllocation =
       call->mir()->to<MCallDOMNative>()->initialHeap() == gc::Heap::Tenured;
   if (preTenureWrapperAllocation) {
@@ -6563,12 +6569,6 @@ void CodeGenerator::visitCallDOMNative(LCallDOMNative* call) {
   static_assert(!JSReturnOperand.aliases(ReturnReg),
                 "Clobbering ReturnReg should not affect the return value");
 
-  // Switch back to the current realm if needed. Note: if the DOM method threw
-  // an exception, the exception handler will do this.
-  if (call->mir()->maybeCrossRealm()) {
-    masm.switchToRealm(gen->realm->realmPtr(), ReturnReg);
-  }
-
   // Wipe out the preTenuring bit from the local alloc site
   // On exception we handle this in C++
   if (preTenureWrapperAllocation) {
@@ -7051,6 +7051,12 @@ void JitRuntime::generateIonGenericCallNativeFunction(MacroAssembler& masm,
   // Push a value containing the callee, which will become argv[0].
   masm.pushValue(JSVAL_TYPE_OBJECT, calleeReg);
 
+  // Load argv into scratch2.
+  masm.moveStackPtrTo(scratch2);
+
+  // Initialize |NativeExitFrameLayout::savedCallee_|.
+  masm.push(calleeReg);
+
   // Load the callee address into calleeReg.
 #ifdef JS_SIMULATOR
   masm.movePtr(ImmPtr(RedirectedCallAnyNative()), calleeReg);
@@ -7059,9 +7065,6 @@ void JitRuntime::generateIonGenericCallNativeFunction(MacroAssembler& masm,
                    calleeReg);
 #endif
 
-  // Load argv into scratch2.
-  masm.moveStackPtrTo(scratch2);
-
   // Push argc.
   masm.push(argcReg);
 
diff --git a/js/src/jit/IonCacheIRCompiler.cpp b/js/src/jit/IonCacheIRCompiler.cpp
index 950181be43c..2e70eb2b5bf 100644
--- a/js/src/jit/IonCacheIRCompiler.cpp
+++ b/js/src/jit/IonCacheIRCompiler.cpp
@@ -1211,6 +1211,7 @@ bool IonCacheIRCompiler::emitCallNativeGetterResult(
   masm.moveStackPtrTo(argVp.get());
 
   // Push marking data for later use.
+  masm.Push(ImmGCPtr(target));
   masm.Push(argUintN);
   pushStubCodePointer();
 
@@ -1725,6 +1726,7 @@ bool IonCacheIRCompiler::emitCallNativeSetter(ObjOperandId receiverId,
   masm.move32(Imm32(1), argUintN);
 
   // Push marking data for later use.
+  masm.Push(ImmGCPtr(target));
   masm.Push(argUintN);
   pushStubCodePointer();
 
diff --git a/js/src/jit/JitFrames.cpp b/js/src/jit/JitFrames.cpp
index e37d76b1f37..fe84b525d90 100644
--- a/js/src/jit/JitFrames.cpp
+++ b/js/src/jit/JitFrames.cpp
@@ -1308,6 +1308,7 @@ static void TraceJitExitFrame(JSTracer* trc, const JSJitFrameIter& frame) {
     size_t len = native->argc() + 2;
     Value* vp = native->vp();
     TraceRootRange(trc, len, vp, "ion-native-args");
+    TraceRoot(trc, native->savedCallee(), "ion-native-saved-callee");
     if (frame.isExitFrameLayout<ConstructNativeExitFrameLayout>()) {
       TraceRoot(trc, vp + len, "ion-native-new-target");
     }
@@ -1319,6 +1320,7 @@ static void TraceJitExitFrame(JSTracer* trc, const JSJitFrameIter& frame) {
         frame.exitFrame()->as<IonOOLNativeExitFrameLayout>();
     TraceRoot(trc, oolnative->stubCode(), "ion-ool-native-code");
     TraceRoot(trc, oolnative->vp(), "iol-ool-native-vp");
+    TraceRoot(trc, oolnative->savedCallee(), "ion-ool-native-saved-callee");
     size_t len = oolnative->argc() + 1;
     TraceRootRange(trc, len, oolnative->thisp(), "ion-ool-native-thisargs");
     return;
diff --git a/js/src/jit/JitFrames.h b/js/src/jit/JitFrames.h
index 7f4b5f6bf31..1373380d563 100644
--- a/js/src/jit/JitFrames.h
+++ b/js/src/jit/JitFrames.h
@@ -684,6 +684,11 @@ class NativeExitFrameLayout {
   ExitFrameLayout exit_;
   uintptr_t argc_;
 
+  // The callee. Unlike the Value below, this isn't overwritten by the return
+  // value. This ensures the callee's realm is kept alive on GC.
+  // See bug 2078837.
+  JSObject* savedCallee_;
+
   // We need to split the Value into 2 fields of 32 bits, otherwise the C++
   // compiler may add some padding between the fields.
   uint32_t loCalleeResult_;
@@ -699,6 +704,7 @@ class NativeExitFrameLayout {
     return reinterpret_cast<JS::Value*>(&loCalleeResult_);
   }
   inline uintptr_t argc() const { return argc_; }
+  inline JSObject** savedCallee() { return &savedCallee_; }
 };
 
 class CallNativeExitFrameLayout : public NativeExitFrameLayout {
@@ -727,6 +733,9 @@ class IonOOLNativeExitFrameLayout {
 
   uintptr_t argc_;
 
+  // See NativeExitFrameLayout::savedCallee_.
+  JSObject* savedCallee_;
+
   // We need to split the Value into 2 fields of 32 bits, otherwise the C++
   // compiler may add some padding between the fields.
   uint32_t loCalleeResult_;
@@ -755,6 +764,7 @@ class IonOOLNativeExitFrameLayout {
   }
   inline JS::Value* thisp() { return reinterpret_cast<JS::Value*>(&loThis_); }
   inline uintptr_t argc() const { return argc_; }
+  inline JSObject** savedCallee() { return &savedCallee_; }
 };
 
 // ProxyGetProperty(JSContext* cx, HandleObject proxy, HandleId id,
diff --git a/js/src/vm/PortableBaselineInterpret.cpp b/js/src/vm/PortableBaselineInterpret.cpp
index 0bdda223452..ec80deec911 100644
--- a/js/src/vm/PortableBaselineInterpret.cpp
+++ b/js/src/vm/PortableBaselineInterpret.cpp
@@ -2304,7 +2304,7 @@ uint64_t ICInterpretOps(uint64_t arg0, uint64_t arg1, ICStub* stub,
         {
           PUSH_IC_FRAME();
 
-          if (!ctx.stack.check(sp, sizeof(StackVal) * (totalArgs + 6))) {
+          if (!ctx.stack.check(sp, sizeof(StackVal) * (totalArgs + 7))) {
             ReportOverRecursed(ctx.frameMgr.cxForLocalUseOnly());
             ctx.error = PBIResult::Error;
             return IC_ERROR_SENTINEL();
@@ -2374,6 +2374,7 @@ uint64_t ICInterpretOps(uint64_t arg0, uint64_t arg1, ICStub* stub,
           Value* args = reinterpret_cast<Value*>(sp);
 
           if (isNative) {
+            PUSHNATIVE(StackValNative(callee));
             PUSHNATIVE(StackValNative(argc));
             PUSHNATIVE(
                 StackValNative(MakeFrameDescriptor(FrameType::BaselineStub)));
diff --git a/js/src/jit-test/tests/ion/bug2078837.js b/js/src/jit-test/tests/ion/bug2078837.js
new file mode 100644
index 00000000000..531d58660af
--- /dev/null
+++ b/js/src/jit-test/tests/ion/bug2078837.js
@@ -0,0 +1,15 @@
+// |jit-test| --no-threads; --fast-warmup
+var fns = [];
+for (var i = 0; i < 5; i++) {
+  fns.push(newGlobal({sameCompartmentAs: this}).callAfterSettingRval);
+}
+for (var i = 0; i < 50; i++) {
+  fns.push(Math.abs, Math.sin, Math.cos, Math.tan,
+            Math.exp, Math.log, Math.sqrt, Math.atan);
+}
+function test() {
+  while (fns.length > 0) {
+    fns.pop()(gc);
+  }
+}
+test();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/js/src/jit-test/tests/ion/bug2078837.js b/js/src/jit-test/tests/ion/bug2078837.js
new file mode 100644
index 00000000000..531d58660af
--- /dev/null
+++ b/js/src/jit-test/tests/ion/bug2078837.js
@@ -0,0 +1,15 @@
+// |jit-test| --no-threads; --fast-warmup
+var fns = [];
+for (var i = 0; i < 5; i++) {
+  fns.push(newGlobal({sameCompartmentAs: this}).callAfterSettingRval);
+}
+for (var i = 0; i < 50; i++) {
+  fns.push(Math.abs, Math.sin, Math.cos, Math.tan,
+            Math.exp, Math.log, Math.sqrt, Math.atan);
+}
+function test() {
+  while (fns.length > 0) {
+    fns.pop()(gc);
+  }
+}
+test();
Loading diff…