Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionJIT miscompilation in the JavaScript Engine: JIT component
ComponentSpiderMonkey
Bug ClassLogic Error
Tracker2044606
Fix commit9a9c26f30a96 (firefox) +85/-0
CISA KEVNot listed
CreditedGary Kwong
Disclosed2026-07-21

Changed Functions

FunctionChangeNotes
if
js/src/jit/BaselineBailouts.cpp
modified
if
js/src/jit/MIR.cpp
modified
for
js/src/jit/MIR.cpp
modified
for
js/src/jit/ScalarReplacement.cpp
modified

Files Changed

  • js/src/jit/BaselineBailouts.cpp
  • js/src/jit/IonTypes.h
  • js/src/jit/MIR.cpp
  • js/src/jit/MIR.h
  • js/src/jit/ScalarReplacement.cpp
diff --git a/js/src/jit/BaselineBailouts.cpp b/js/src/jit/BaselineBailouts.cpp
index 6e2bc4f8146..a7db5affa23 100644
--- a/js/src/jit/BaselineBailouts.cpp
+++ b/js/src/jit/BaselineBailouts.cpp
@@ -25,12 +25,14 @@
 #include "jit/RematerializedFrame.h"
 #include "jit/SharedICRegisters.h"
 #include "jit/Simulator.h"
+#include "jit/VMFunctions.h"
 #include "js/friend/StackLimits.h"  // js::AutoCheckRecursionLimit, js::ReportOverRecursed
 #include "js/Utility.h"
 #include "proxy/ScriptedProxyHandler.h"
 #include "util/Memory.h"
 #include "vm/ArgumentsObject.h"
 #include "vm/BytecodeUtil.h"
+#include "vm/Iteration.h"
 #include "vm/JitActivation.h"
 
 #include "jit/JitFrames-inl.h"
@@ -918,6 +920,25 @@ bool BaselineStackBuilder::buildExpressionStack() {
     }
   }
 
+  if (resumeMode() == ResumeMode::ResumeAfterObjectKeys) {
+    JitSpew(JitSpew_BaselineBailouts,
+            "      Converting Object.keys iterator to keys array");
+    // The result slot holds the internal PropertyIteratorObject produced by the
+    // Object.keys scalar-replacement optimization. Convert it back to the keys
+    // array so the internal iterator is never exposed to the baseline frame.
+    Value iterVal;
+    if (peekLastValue(&iterVal) && !iterVal.isMagic(JS_OPTIMIZED_OUT)) {
+      MOZ_RELEASE_ASSERT(iterVal.isObject());
+      MOZ_RELEASE_ASSERT(iterVal.toObject().is<PropertyIteratorObject>());
+      RootedObject iterObj(cx_, &iterVal.toObject());
+      JSObject* keys = ObjectKeysFromIterator(cx_, iterObj);
+      if (!keys) {
+        return false;
+      }
+      valuePointerAtStackOffset(0).set(ObjectValue(*keys));
+    }
+  }
+
   return true;
 }
 
diff --git a/js/src/jit/IonTypes.h b/js/src/jit/IonTypes.h
index 8c42cb63263..9de8e5574e1 100644
--- a/js/src/jit/IonTypes.h
+++ b/js/src/jit/IonTypes.h
@@ -795,6 +795,15 @@ enum class ResumeMode : uint8_t {
   // of a proxy get trap aligns with what the spec requires.
   ResumeAfterCheckProxyGetResult,
 
+  // Innermost frame. Resume at the next bytecode op when bailing out, but the
+  // value in the result slot is the internal PropertyIteratorObject created by
+  // the Object.keys scalar-replacement optimization instead of the keys array.
+  // On bailout we convert it back to the keys array so the internal iterator is
+  // never exposed to the baseline frame. This is used when the
+  // MObjectToIterator
+  // VM call bails out (e.g. an invalidation bailout caused by GC).
+  ResumeAfterObjectKeys,
+
   // Innermost frame. Resume at the current bytecode op when bailing out.
   ResumeAt,
 
@@ -826,6 +835,8 @@ inline const char* ResumeModeToString(ResumeMode mode) {
       return "ResumeAfterCheckIsObject";
     case ResumeMode::ResumeAfterCheckProxyGetResult:
       return "ResumeAfterCheckProxyGetResult";
+    case ResumeMode::ResumeAfterObjectKeys:
+      return "ResumeAfterObjectKeys";
   }
   MOZ_CRASH("Invalid mode");
 }
@@ -835,6 +846,7 @@ inline bool IsResumeAfter(ResumeMode mode) {
     case ResumeMode::ResumeAfter:
     case ResumeMode::ResumeAfterCheckIsObject:
     case ResumeMode::ResumeAfterCheckProxyGetResult:
+    case ResumeMode::ResumeAfterObjectKeys:
       return true;
     default:
       return false;
diff --git a/js/src/jit/MIR.cpp b/js/src/jit/MIR.cpp
index 0daa5f95bef..dbf9fa5f222 100644
--- a/js/src/jit/MIR.cpp
+++ b/js/src/jit/MIR.cpp
@@ -714,6 +714,16 @@ void MInstruction::stealResumePoint(MInstruction* other) {
   setResumePoint(resumePoint);
 }
 
+bool MInstruction::copyResumePointFrom(TempAllocator& alloc,
+                                       MInstruction* previous) {
+  MResumePoint* rp = previous->resumePoint_->clone(alloc);
+  if (!rp) {
+    return false;
+  }
+  setResumePoint(rp);
+  return true;
+}
+
 void MInstruction::moveResumePointAsEntry() {
   MOZ_ASSERT(isNop());
   block()->clearEntryResumePoint();
@@ -4412,6 +4422,19 @@ MResumePoint* MResumePoint::New(TempAllocator& alloc, MBasicBlock* block,
   return resume;
 }
 
+MResumePoint* MResumePoint::clone(TempAllocator& alloc) {
+  MResumePoint* resume = new (alloc) MResumePoint(block(), pc_, mode_);
+  size_t n = this->numOperands();
+  if (!resume->operands_.init(alloc, n)) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < n; i++) {
+    resume->initOperand(i, getOperand(i));
+  }
+  resume->stores_.copy(this->stores_);
+  return resume;
+}
+
 MResumePoint::MResumePoint(MBasicBlock* block, jsbytecode* pc, ResumeMode mode)
     : MNode(block, Kind::ResumePoint),
       pc_(pc),
diff --git a/js/src/jit/MIR.h b/js/src/jit/MIR.h
index 8e909e049f7..d1973605eaa 100644
--- a/js/src/jit/MIR.h
+++ b/js/src/jit/MIR.h
@@ -1065,6 +1065,9 @@ class MInstruction : public MDefinition, public InlineListNode<MInstruction> {
 
   void setResumePoint(MResumePoint* resumePoint);
   void stealResumePoint(MInstruction* other);
+  // Copy resume point from the previous instruction.
+  [[nodiscard]] bool copyResumePointFrom(TempAllocator& alloc,
+                                         MInstruction* previous);
 
   void moveResumePointAsEntry();
   void clearResumePoint();
@@ -8878,6 +8881,9 @@ class MResumePoint final : public MNode
  public:
   static MResumePoint* New(TempAllocator& alloc, MBasicBlock* block,
                            jsbytecode* pc, ResumeMode mode);
+  // NOTE: instruction_ is left null; call setResumePoint to associate the
+  // clone with an instruction.
+  [[nodiscard]] MResumePoint* clone(TempAllocator& alloc);
 
   MBasicBlock* block() const { return resumePointBlock(); }
 
@@ -8927,6 +8933,7 @@ class MResumePoint final : public MNode
     instruction_ = nullptr;
   }
   ResumeMode mode() const { return mode_; }
+  void setMode(ResumeMode mode) { mode_ = mode; }
 
   void releaseUses() {
     for (size_t i = 0, e = numOperands(); i < e; i++) {
diff --git a/js/src/jit/ScalarReplacement.cpp b/js/src/jit/ScalarReplacement.cpp
index 140b90eb8db..8bcdbff5b89 100644
--- a/js/src/jit/ScalarReplacement.cpp
+++ b/js/src/jit/ScalarReplacement.cpp
@@ -4491,6 +4491,28 @@ bool ObjectKeysReplacer::run(MInstructionIterator& outerIterator) {
 
   auto* forRecovery = MObjectKeysFromIterator::New(alloc_, objToIter_);
   arr_->block()->insertBefore(arr_, forRecovery);
+
+  auto* nop = MNop::New(alloc_);
+  arr_->block()->insertBefore(arr_, nop);
+  if (!nop->copyResumePointFrom(alloc_, objToIter_)) {
+    return false;
+  }
+
+  {
+    // Use the PropertyIteratorObject in the resume point for the
+    // MObjectToIterator instruction. If this instruction calls a VM function
+    // that triggers an invalidation, we use ResumeMode::ResumeAfterObjectKeys
+    // to create the array from the iterator object when we bail out.
+    MResumePoint* rp = objToIter_->resumePoint();
+    size_t n = rp->numOperands() - 1;
+    for (size_t i = 0; i < n; i++) {
+      MOZ_RELEASE_ASSERT(rp->getOperand(i) != arr_);
+    }
+    MOZ_RELEASE_ASSERT(rp->getOperand(n) == arr_);
+    rp->replaceOperand(n, objToIter_);
+    MOZ_RELEASE_ASSERT(rp->mode() == ResumeMode::ResumeAfter);
+    rp->setMode(ResumeMode::ResumeAfterObjectKeys);
+  }
   arr_->replaceAllUsesWith(forRecovery);
 
   // We need to explicitly discard the instruction since it's marked as
Loading diff…