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
Tracker2074632
Fix commit068cf1d361bd (firefox) +73/-0
CISA KEVNot listed
CreditedTomer Fichman
Disclosed2026-10-13

Files Changed

  • js/src/jit/CodeGenerator.cpp
  • js/src/jit/CodeGenerator.h
  • js/src/jit/Ion.cpp
  • js/src/jit/JitRuntime.h
  • js/src/vm/Interpreter.cpp
  • js/src/vm/NativeObject.cpp
diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
index b7fa075d98b..ceb19ca60bf 100644
--- a/js/src/jit/CodeGenerator.cpp
+++ b/js/src/jit/CodeGenerator.cpp
@@ -6443,6 +6443,19 @@ static void LoadDOMPrivate(MacroAssembler& masm, Register obj, Register priv,
   }
 }
 
+void CodeGenerator::maybeEmitSetInPureCall(MInstruction* mir, bool value,
+                                           Register scratch) {
+  AliasSet aliasSet = mir->getAliasSet();
+  if (aliasSet.isStore() && (aliasSet.flags() & AliasSet::ObjectFields) != 0) {
+    // If the alias set already indicates that this can reallocate slots, the
+    // flag is unnecessary.
+    return;
+  }
+  const void* addr = gen->jitRuntime()->addressOfInPureCall();
+  masm.move32(Imm32(value), scratch);
+  masm.store32(scratch, AbsoluteAddress(addr));
+}
+
 void CodeGenerator::visitCallDOMNative(LCallDOMNative* call) {
   WrappedFunction* target = call->getSingleTarget();
   MOZ_ASSERT(target);
@@ -6530,6 +6543,8 @@ void CodeGenerator::visitCallDOMNative(LCallDOMNative* call) {
 
   markSafepointAt(safepointOffset, call);
 
+  maybeEmitSetInPureCall(call->mir(), true, /*scratch =*/argJSContext);
+
   // Construct and execute call.
   masm.setupAlignedABICall();
   masm.loadJSContext(argJSContext);
@@ -6542,6 +6557,9 @@ void CodeGenerator::visitCallDOMNative(LCallDOMNative* call) {
                    ABIType::General,
                    CheckUnsafeCallWithABI::DontCheckHasExitFrame);
 
+  Register notReturnReg = argJSContext == ReturnReg ? argObj : argJSContext;
+  maybeEmitSetInPureCall(call->mir(), false, /*scratch =*/notReturnReg);
+
   if (target->jitInfo()->isInfallible) {
     masm.loadValue(Address(masm.getStackPointer(),
                            IonDOMMethodExitFrameLayout::offsetOfResult()),
@@ -20362,6 +20380,8 @@ void CodeGenerator::visitGetDOMProperty(LGetDOMProperty* ins) {
 
   markSafepointAt(safepointOffset, ins);
 
+  maybeEmitSetInPureCall(ins->mir(), true, /*scratch =*/JSContextReg);
+
   masm.setupAlignedABICall();
   masm.loadJSContext(JSContextReg);
   masm.passABIArg(JSContextReg);
@@ -20373,6 +20393,9 @@ void CodeGenerator::visitGetDOMProperty(LGetDOMProperty* ins) {
                    ABIType::General,
                    CheckUnsafeCallWithABI::DontCheckHasExitFrame);
 
+  Register notReturnReg = JSContextReg == ReturnReg ? ObjectReg : JSContextReg;
+  maybeEmitSetInPureCall(ins->mir(), false, /*scratch =*/notReturnReg);
+
   if (ins->mir()->isInfallible()) {
     masm.loadValue(Address(masm.getStackPointer(),
                            IonDOMExitFrameLayout::offsetOfResult()),
diff --git a/js/src/jit/CodeGenerator.h b/js/src/jit/CodeGenerator.h
index 58c047dea41..59198885c38 100644
--- a/js/src/jit/CodeGenerator.h
+++ b/js/src/jit/CodeGenerator.h
@@ -469,6 +469,8 @@ class CodeGenerator final : public CodeGeneratorSpecific {
     }
     return false;
   }
+
+  void maybeEmitSetInPureCall(MInstruction* mir, bool value, Register scratch);
 };
 
 }  // namespace jit
diff --git a/js/src/jit/Ion.cpp b/js/src/jit/Ion.cpp
index 93b36a4806b..bdee800915a 100644
--- a/js/src/jit/Ion.cpp
+++ b/js/src/jit/Ion.cpp
@@ -70,6 +70,7 @@
 #include "gc/GC-inl.h"
 #include "gc/StableCellHasher-inl.h"
 #include "jit/InlineScriptTree-inl.h"
+#include "jit/JSJitFrameIter-inl.h"
 #include "jit/MacroAssembler-inl.h"
 #include "jit/SafepointIndex-inl.h"
 #include "vm/GeckoProfiler-inl.h"
@@ -433,6 +434,34 @@ void JitRuntime::TraceWeakJitcodeGlobalTable(JSRuntime* rt, JSTracer* trc) {
   }
 }
 
+void JitRuntime::handleGrowSlotsForPureCall(JSContext* cx) {
+  MOZ_ASSERT(inPureCall());
+
+  // We only set the inPureCall flag for calls in Ion code with alias
+  // sets that claim not to have arbitrary side-effects. Therefore, we
+  // should not have re-entered JIT code, and the call that set the
+  // flag should be the innermost frame (aside from the exit frame).
+  MOZ_RELEASE_ASSERT(cx->activation()->isJit());
+  JitActivation* activation = cx->activation()->asJit();
+  MOZ_RELEASE_ASSERT(activation->hasExitFP());
+
+  JSJitFrameIter frame(activation);
+  // We currently only set this flag for DOM calls.
+  MOZ_RELEASE_ASSERT(frame.isExitFrameLayout<IonDOMExitFrameLayout>());
+  ++frame;
+  MOZ_RELEASE_ASSERT(frame.isIonJS());
+
+  // If we've already invalidated, we've cleared the IonScript
+  // pointer, so we can't invalidate again.
+  if (frame.checkInvalidation()) {
+    return;
+  }
+
+  JitSpew(JitSpew_IonInvalidate,
+          "Invalidating frame: slots reallocated during pure call");
+  Invalidate(cx, frame.script());
+}
+
 bool JitZone::addInlinedCompilation(const IonScriptKey& ionScriptKey,
                                     JSScript* inlined) {
   MOZ_ASSERT(inlined != ionScriptKey.script());
diff --git a/js/src/jit/JitRuntime.h b/js/src/jit/JitRuntime.h
index 5754a839afa..25b92d8e01b 100644
--- a/js/src/jit/JitRuntime.h
+++ b/js/src/jit/JitRuntime.h
@@ -240,6 +240,11 @@ class JitRuntime {
   MainThreadData<uint32_t> disallowArbitraryCode_{false};
 #endif
 
+  // Flag that can be set from JIT code to indicate that we're calling
+  // a function whose alias set does not include stores to ObjectFields.
+  // See handleGrowSlotsForPureCall.
+  MainThreadData<uint32_t> inPureCall_{false};
+
   bool generateTrampolines(JSContext* cx);
   bool generateBaselineICFallbackCode(JSContext* cx);
 
@@ -361,6 +366,9 @@ class JitRuntime {
   }
 #endif
 
+  bool inPureCall() const { return inPureCall_; }
+  const void* addressOfInPureCall() const { return &inPureCall_.refNoCheck(); }
+
   uint8_t* allocateIonOsrTempData(size_t size);
   void freeIonOsrTempData();
 
@@ -515,6 +523,8 @@ class JitRuntime {
 
   void ionLazyLinkListRemove(JSRuntime* rt, js::jit::IonCompileTask* task);
   void ionLazyLinkListAdd(JSRuntime* rt, js::jit::IonCompileTask* task);
+
+  void handleGrowSlotsForPureCall(JSContext* cx);
 };
 
 }  // namespace jit
diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp
index 174fc38056c..c03af9d42bd 100644
--- a/js/src/vm/Interpreter.cpp
+++ b/js/src/vm/Interpreter.cpp
@@ -441,6 +441,8 @@ bool js::RunScript(JSContext* cx, RunState& state) {
 
   MOZ_ASSERT_IF(cx->runtime()->hasJitRuntime(),
                 !cx->runtime()->jitRuntime()->disallowArbitraryCode());
+  MOZ_ASSERT_IF(cx->runtime()->hasJitRuntime(),
+                !cx->runtime()->jitRuntime()->inPureCall());
 
   // Since any script can conceivably GC, make sure it's safe to do so.
   cx->verifyIsSafeToGC();
diff --git a/js/src/vm/NativeObject.cpp b/js/src/vm/NativeObject.cpp
index 7bba6b1f5b0..e828f188cd3 100644
--- a/js/src/vm/NativeObject.cpp
+++ b/js/src/vm/NativeObject.cpp
@@ -9,6 +9,7 @@
 
 #include "gc/MaybeRooted.h"
 #include "gc/StableCellHasher.h"
+#include "jit/JitRuntime.h"
 #include "js/friend/ErrorMessages.h"  // js::GetErrorMessage, JSMSG_*
 #include "js/friend/StackLimits.h"    // js::AutoCheckRecursionLimit
 #include "js/Printer.h"               // js::GenericPrinter
@@ -357,6 +358,12 @@ bool NativeObject::growSlots(JSContext* cx, uint32_t oldCapacity,
   gc::MemoryReleaseFence(zone());
   slots_ = newSlots;
 
+  if (cx->runtime()->hasJitRuntime() &&
+      MOZ_UNLIKELY(cx->runtime()->jitRuntime()->inPureCall()) &&
+      is<GlobalObject>()) {
+    cx->runtime()->jitRuntime()->handleGrowSlotsForPureCall(cx);
+  }
+
   MOZ_ASSERT(hasDynamicSlots());
   return true;
 }
Loading diff…