CVE-2026-16355
Overview
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
Activeflag and resetting its property cursor. - `extractCurrentIndexAndKindFromIterator`
- the generated fast-path code that loads
NativeIteratorstate and indexes intoindices[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.
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
- Craft JS that closes then reuses an iterator
Author JavaScript where a
for-in/iterator is ended along one control-flow path and itsnext-derived value is still consumed on a path dominated by that end. - Trigger Ion compilation
Run the function hot enough for Ion to compile it and invoke
OptimizeIteratorIndices. - Get the unsafe rewrite
The pass optimizes the
MIteratorMoreinto theextractCurrentIndexAndKindFromIteratorfast path despite the dominatingMIteratorEnd. - Execute on a closed iterator
At runtime the fast path runs against a
NativeIteratorwhose cursor is0, computingindices[cursor - 1]and reading out of bounds. - Leverage the OOB read The out-of-bounds index feeds subsequent property-access logic, yielding a miscompilation-driven memory-safety violation.
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
forjs/src/jit/IonAnalysis.cpp |
modified | |
ifjs/src/jit/IonAnalysis.cpp |
modified |
Files Changed
js/src/jit/IonAnalysis.cppjs/src/jit/MacroAssembler.cpp
Audit Directions
- Dominance-unaware JIT rewritesAudit 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 gapsReview generated code that assumes a live/active object state (
Activeflags, 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 underflowSearch 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.
Patch
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);