CVE-2026-10702
Overview
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.cpppass that removes heap allocations by replacing them with recoverable instructions, here rewritingObject.keys()throughObjectKeysReplacer. - Resume Point
- a snapshot of interpreter state (
can_recover: true) attached to an instruction so the correct value can be rebuilt on bailout;stealResumePointtransfers 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.
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
- Trigger JIT tiering
Attacker-controlled JavaScript runs a hot loop that calls
Object.keys()on a suitable object until IonMonkey compiles it. - Force scalar replacement
The shape of the code causes
ObjectKeysReplacerto fire, emitting the skip-registrationMObjectToIteratorwith its incorrect alias set. - Provoke miscompilation
Downstream passes (GVN/LICM/reordering) act on the false
Loadalias information and/or the mis-placed resume point, producing machine code that computes or recovers the wrong result. - Induce bailout or divergence
Execution paths that rely on the recoverable state (e.g. a bailout) rebuild an inconsistent
Object.keysresult, breaking an engine invariant. - Leverage the corruption The attacker turns the type/state inconsistency into a memory-safety primitive within the JIT-compiled code.
Impact Assessment
ObjectKeysReplacer scalar-replacement path.Files Changed
js/src/jit/MIR.hjs/src/jit/MIROps.yamljs/src/jit/ScalarReplacement.cpp
Audit Directions
- Alias-set vs. real effectsReview every MIR instruction whose
getAliasSetis conditional or hand-written and confirm the declaredLoad/Store/nonematches the instruction’s actual runtime memory behavior after any transform that creates it. - Resume-point ownershipIn scalar-replacement and other recover-based transforms, verify that
stealResumePoint/can_recoversnapshots are attached to the exact instruction that reconstructs the value, not to a sibling produced in the same rewrite. - skipRegistration / synthesized nodesAudit 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.
Patch
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