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
Tracker2040903
Fix commite43e6780c6c0 (firefox) +2/-7
CISA KEVNot listed
CreditedNebula Security
Disclosed2026-06-02

Background

`MObjectToIterator`
a SpiderMonkey MIR instruction that turns an object into an iterator, synthesized during scalar replacement of Object.keys().
`AliasSet`
the JIT’s summary of which memory an instruction reads (Load) or writes (Store), used by GVN, LICM, and reordering passes to decide what is safe to move, merge, or eliminate.
Scalar Replacement
the ScalarReplacement.cpp pass that removes heap allocations by replacing them with recoverable instructions, here rewriting Object.keys() through ObjectKeysReplacer.
Resume Point
a snapshot of interpreter state (can_recover: true) attached to an instruction so the correct value can be rebuilt on bailout; stealResumePoint transfers that snapshot from one instruction to another.

Root Cause Analysis

When ObjectKeysReplacer::run scalar-replaces an Object.keys() call, it creates an MObjectToIterator with setSkipRegistration(true) plus an MObjectKeysFromIterator (forRecovery) that carries the recoverable result. The vulnerable code had MObjectToIterator::getAliasSet report AliasSet::Load(ObjectFields | Element) in the skip-registration case, which told alias analysis the instruction reads object memory even though after this transform it is a pure, recover-only node with no runtime memory effect. Reporting a bogus memory dependency let optimization passes reason about ordering and elimination incorrectly, and the resume point needed for correct recovery was attached to forRecovery rather than to the MObjectToIterator that actually needs it, so a bailout could reconstruct the wrong state.

The fix removes the hand-written getAliasSet override and declares alias_set: none in MIROps.yaml, making the instruction correctly aliasless, and moves stealResumePoint(arr_) onto objToIter_ so the recoverable node owns its own snapshot. Together these restore the invariant that an instruction’s declared alias set and resume-point ownership match its real effects, so the optimizer no longer miscompiles the sequence.

Key insight
The single mistake was MObjectToIterator advertising a memory alias set (Load(ObjectFields | Element)) and having its recovery snapshot on the wrong instruction, so the JIT’s alias analysis operated on false effect information; the fix makes the node truly aliasless (alias_set: none) and gives it the resume point it needs to recover correctly.

Attack Path

  1. Trigger JIT tiering Attacker-controlled JavaScript runs a hot loop that calls Object.keys() on a suitable object until IonMonkey compiles it.
  2. Force scalar replacement The shape of the code causes ObjectKeysReplacer to fire, emitting the skip-registration MObjectToIterator with its incorrect alias set.
  3. Provoke miscompilation Downstream passes (GVN/LICM/reordering) act on the false Load alias information and/or the mis-placed resume point, producing machine code that computes or recovers the wrong result.
  4. Induce bailout or divergence Execution paths that rely on the recoverable state (e.g. a bailout) rebuild an inconsistent Object.keys result, breaking an engine invariant.
  5. Leverage the corruption The attacker turns the type/state inconsistency into a memory-safety primitive within the JIT-compiled code.

Impact Assessment

An attacker who can run JavaScript in the content process gains a JIT miscompilation that violates engine state invariants, a class that typically yields type confusion or memory corruption exploitable for control of the process. It executes in the JavaScript engine within the web content sandbox, requiring only that the victim load an attacker-controlled page. The sole precondition is that the crafted script trigger Ion compilation and the ObjectKeysReplacer scalar-replacement path.

Files Changed

  • js/src/jit/MIR.h
  • js/src/jit/MIROps.yaml
  • js/src/jit/ScalarReplacement.cpp

Audit Directions

  • Alias-set vs. real effects
    Review every MIR instruction whose getAliasSet is conditional or hand-written and confirm the declared Load/Store/none matches the instruction’s actual runtime memory behavior after any transform that creates it.
  • Resume-point ownership
    In scalar-replacement and other recover-based transforms, verify that stealResumePoint / can_recover snapshots are attached to the exact instruction that reconstructs the value, not to a sibling produced in the same rewrite.
  • skipRegistration / synthesized nodes
    Audit instructions created with flags like setSkipRegistration(true) for divergence between their optimizer-visible model and their real fast-path semantics that could enable illegal reordering or elimination.
diff --git a/js/src/jit/MIR.h b/js/src/jit/MIR.h
index 40fc388fc06..563a5fb3131 100644
--- a/js/src/jit/MIR.h
+++ b/js/src/jit/MIR.h
@@ -9362,12 +9362,6 @@ class MObjectToIterator : public MUnaryInstruction,
   TRIVIAL_NEW_WRAPPERS
   NAMED_OPERANDS((0, object))
 
-  AliasSet getAliasSet() const override {
-    return skipRegistration_
-               ? AliasSet::Load(AliasSet::ObjectFields | AliasSet::Element)
-               : AliasSet::Store(AliasSet::Any);
-  }
-
   bool wantsIndices() const { return wantsIndices_; }
   void setWantsIndices(bool value) { wantsIndices_ = value; }
 
diff --git a/js/src/jit/MIROps.yaml b/js/src/jit/MIROps.yaml
index 5bb075642e8..5b08566e9be 100644
--- a/js/src/jit/MIROps.yaml
+++ b/js/src/jit/MIROps.yaml
@@ -2377,6 +2377,7 @@
   operands:
     iterator: Object
   result_type: Object
+  alias_set: none
   can_recover: true
 
 - name: LoadUnboxedScalar
diff --git a/js/src/jit/ScalarReplacement.cpp b/js/src/jit/ScalarReplacement.cpp
index dee8891d7bc..0e225c9f9b6 100644
--- a/js/src/jit/ScalarReplacement.cpp
+++ b/js/src/jit/ScalarReplacement.cpp
@@ -4430,6 +4430,7 @@ bool ObjectKeysReplacer::run(MInstructionIterator& outerIterator) {
 
   objToIter_ = MObjectToIterator::New(alloc_, objectKeys()->object(), nullptr);
   objToIter_->setSkipRegistration(true);
+  objToIter_->stealResumePoint(arr_);
   arr_->block()->insertBefore(arr_, objToIter_);
 
   // Iterate over each basic block.
@@ -4464,7 +4465,6 @@ bool ObjectKeysReplacer::run(MInstructionIterator& outerIterator) {
 
   auto* forRecovery = MObjectKeysFromIterator::New(alloc_, objToIter_);
   arr_->block()->insertBefore(arr_, forRecovery);
-  forRecovery->stealResumePoint(arr_);
   arr_->replaceAllUsesWith(forRecovery);
 
   // We need to explicitly discard the instruction since it's marked as
Loading diff…