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
Tracker2052207
Fix commitdc03ed6e0053 (firefox) +107/-0
CISA KEVNot listed
CreditedAmy Burnett of OpenAI
Disclosed2026-07-21

Background

`OptimizeIteratorIndices`
an Ion JIT graph pass that rewrites for-in/iterator property accesses into a direct fast path that reads the iterator’s cached index array.
`MIteratorMore`
the MIR instruction that advances a native iterator and returns the next key, and whose result the index optimization keys off of.
`MIteratorEnd`
the MIR instruction that closes a native iterator, clearing its Active flag and resetting its property cursor.
`extractCurrentIndexAndKindFromIterator`
the generated fast-path code that loads NativeIterator state and indexes into indices[cursor - 1].

Root Cause Analysis

The OptimizeIteratorIndices pass converted an iterator next access (MIteratorMore) into the extractCurrentIndexAndKindFromIterator fast path without verifying that the underlying iterator was still active at the point of use. That fast path computes indices[cursor - 1], which requires the invariant that the NativeIterator is still Active and its PropertyCursor is non-zero; but a use dominated by an MIteratorEnd for the same iterator operates on a closed iterator whose cursor has been reset to 0, so the optimized load reads indices[-1] out of bounds.

The fix adds FindSafeIteratorMoreInstructions, which walks each MObjectToIterator and only whitelists MIteratorMore instructions none of whose uses are dominated by a matching MIteratorEnd; OptimizeIteratorIndices then skips any iterNext not in safeIteratorMores. This restores the invariant that the fast path only runs on iterators still guaranteed active at that program point, and DEBUG assertions on the Active flag and a non-zero cursor were added to extractCurrentIndexAndKindFromIterator to catch future regressions.

Key insight
The core mistake was applying the iterator-index fast path based purely on def-use shape without checking dominance against iterator-close (MIteratorEnd), letting a closed iterator with a zeroed cursor reach code that assumes it is active. The fix gates the optimization on a dominance analysis that excludes any MIteratorMore whose use can be reached after the iterator is ended.

Attack Path

  1. Craft JS that closes then reuses an iterator Author JavaScript where a for-in/iterator is ended along one control-flow path and its next-derived value is still consumed on a path dominated by that end.
  2. Trigger Ion compilation Run the function hot enough for Ion to compile it and invoke OptimizeIteratorIndices.
  3. Get the unsafe rewrite The pass optimizes the MIteratorMore into the extractCurrentIndexAndKindFromIterator fast path despite the dominating MIteratorEnd.
  4. Execute on a closed iterator At runtime the fast path runs against a NativeIterator whose cursor is 0, computing indices[cursor - 1] and reading out of bounds.
  5. Leverage the OOB read The out-of-bounds index feeds subsequent property-access logic, yielding a miscompilation-driven memory-safety violation.

Impact Assessment

An attacker who can run scripted content gains a JIT-miscompilation memory-safety bug in the SpiderMonkey content process, stemming from an out-of-bounds read of the iterator indices array on a closed iterator. Exploitation requires only that a page trigger Ion compilation of a suitably shaped iterator pattern, matching the metadata’s high severity/impact for a JavaScript-engine logic error. Effects are confined to the JS engine’s process context as reachable via web content.

Changed Functions

FunctionChangeNotes
for
js/src/jit/IonAnalysis.cpp
modified
if
js/src/jit/IonAnalysis.cpp
modified

Files Changed

  • js/src/jit/IonAnalysis.cpp
  • js/src/jit/MacroAssembler.cpp

Audit Directions

  • Dominance-unaware JIT rewrites
    Audit other Ion optimizations that fold state-dependent instructions (iterators, generators, cached slots) into fast paths without checking dominance against a state-invalidating instruction.
  • Fast-path invariant gaps
    Review generated code that assumes a live/active object state (Active flags, non-zero cursors/lengths) and confirm those invariants are enforced by the graph pass that emits the fast path, not merely assumed.
  • Cursor and length underflow
    Search for cursor - 1, index-minus-one, or count-based array accesses in JIT/native-iterator code where a reset-to-zero value could underflow to an out-of-bounds index.
diff --git a/js/src/jit/IonAnalysis.cpp b/js/src/jit/IonAnalysis.cpp
index a2c88692e66..36597cc8752 100644
--- a/js/src/jit/IonAnalysis.cpp
+++ b/js/src/jit/IonAnalysis.cpp
@@ -14,6 +14,7 @@
 #include "jit/DominatorTree.h"
 #include "jit/MIRGenerator.h"
 #include "jit/MIRGraph.h"
+#include "js/HashTable.h"
 
 #include "vm/BytecodeUtil-inl.h"
 
@@ -2485,6 +2486,85 @@ static MObjectToIterator* FindObjectToIteratorUse(MDefinition* ins) {
   return nullptr;
 }
 
+using IteratorMoreSet =
+    InlineSet<MIteratorMore*, 8, DefaultHasher<MIteratorMore*>,
+              BackgroundSystemAllocPolicy>;
+
+static bool FindSafeIteratorMoreInstructions(MIRGraph& graph,
+                                             IteratorMoreSet& safeIterMores) {
+  // Fill |safeIterMores| with MIteratorMore instructions where no instruction
+  // use is dominated by an MIteratorEnd for the same iterator.
+
+  using InstructionVector =
+      Vector<MInstruction*, 8, BackgroundSystemAllocPolicy>;
+
+  auto hasDominatingIteratorEnd = [](const InstructionVector& iteratorEnds,
+                                     MInstruction* access) {
+    for (MInstruction* iteratorEnd : iteratorEnds) {
+      if (iteratorEnd->dominates(access)) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  for (MBasicBlockIterator block(graph.begin()); block != graph.end();
+       block++) {
+    for (MInstructionIterator ins(block->begin()); ins != block->end(); ins++) {
+      if (!ins->isObjectToIterator()) {
+        continue;
+      }
+
+      InstructionVector iteratorMores;
+      InstructionVector iteratorEnds;
+      bool hasPhiUse = false;
+
+      for (MUseDefIterator uses(*ins); uses; uses++) {
+        MDefinition* def = uses.def();
+        if (def->isIteratorMore()) {
+          if (!iteratorMores.append(def->toInstruction())) {
+            return false;
+          }
+        } else if (def->isIteratorEnd()) {
+          if (!iteratorEnds.append(def->toInstruction())) {
+            return false;
+          }
+        } else if (def->isLoadIteratorElement() ||
+                   def->isObjectKeysFromIterator() || def->isIteratorLength() ||
+                   def->isPostWriteBarrier() || def->isStoreElement()) {
+          continue;
+        } else if (def->isPhi()) {
+          hasPhiUse = true;
+          break;
+        } else {
+          MOZ_CRASH("Unexpected ObjectToIterator use");
+        }
+      }
+      if (hasPhiUse) {
+        continue;
+      }
+
+      for (MInstruction* iterMore : iteratorMores) {
+        bool hasUnsafeUse = false;
+        for (MUseDefIterator iterMoreUses(iterMore); iterMoreUses;
+             iterMoreUses++) {
+          MDefinition* def = iterMoreUses.def();
+          if (def->isInstruction() &&
+              hasDominatingIteratorEnd(iteratorEnds, def->toInstruction())) {
+            hasUnsafeUse = true;
+            break;
+          }
+        }
+        if (!hasUnsafeUse && !safeIterMores.put(iterMore->toIteratorMore())) {
+          return false;
+        }
+      }
+    }
+  }
+
+  return true;
+}
+
 bool jit::OptimizeIteratorIndices(const MIRGenerator* mir, MIRGraph& graph) {
   bool changed = false;
 
@@ -2493,6 +2573,11 @@ bool jit::OptimizeIteratorIndices(const MIRGenerator* mir, MIRGraph& graph) {
     return block->id() >= numInitialBlocks;
   };
 
+  IteratorMoreSet safeIteratorMores;
+  if (!FindSafeIteratorMoreInstructions(graph, safeIteratorMores)) {
+    return false;
+  }
+
   for (ReversePostorderIterator blockIter = graph.rpoBegin();
        blockIter != graph.rpoEnd();) {
     MBasicBlock* block = *blockIter++;
@@ -2594,6 +2679,9 @@ bool jit::OptimizeIteratorIndices(const MIRGenerator* mir, MIRGraph& graph) {
             SkipIterObjectUnbox(receiver)) {
           continue;
         }
+        if (!safeIteratorMores.has(iterNext)) {
+          continue;
+        }
       } else if (supportObjectKeys && SkipBox(idVal)->isLoadIteratorElement()) {
         auto* iterLoad = SkipBox(idVal)->toLoadIteratorElement();
 
diff --git a/js/src/jit/MacroAssembler.cpp b/js/src/jit/MacroAssembler.cpp
index 32ee888f311..7afb8570506 100644
--- a/js/src/jit/MacroAssembler.cpp
+++ b/js/src/jit/MacroAssembler.cpp
@@ -3160,9 +3160,28 @@ void MacroAssembler::extractCurrentIndexAndKindFromIterator(Register iterator,
                          PropertyIteratorObject::offsetOfIteratorSlot());
   loadPrivate(nativeIterAddr, outIndex);
 
+#ifdef DEBUG
+  // Assert the Active flag is set.
+  Label iterActive;
+  branchTest32(Assembler::NonZero,
+               Address(outIndex, NativeIterator::offsetOfFlags()),
+               Imm32(NativeIterator::Flags::Active), &iterActive);
+  assumeUnreachable("iterator-index fast path on an inactive iterator");
+  bind(&iterActive);
+#endif
+
   // Load the property count into outKind.
   load32(Address(outIndex, NativeIterator::offsetOfPropertyCount()), outKind);
 
+  // The cursor must not be 0 because then we would access indices[cursor - 1]
+  // below.
+  Label cursorOk;
+  branch32(Assembler::NotEqual,
+           Address(outIndex, NativeIterator::offsetOfPropertyCursor()),
+           Imm32(0), &cursorOk);
+  assumeUnreachable("iterator-index fast path on a closed iterator");
+  bind(&cursorOk);
+
   // We need two bits of wiggle room in a u32 here for the logic below.
   static_assert(NativeIterator::PropCountLimit <= 1 << 30);
 
Loading diff…