Medium CVSS 6.5 firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionIt was possible to interrupt the processing of a RegExp bailout and run additional JavaScript, potentially triggering garbage collection when the engine was not expecting it.
ComponentSpiderMonkey
Bug ClassLogic Error
Tracker1942881
Fix commitac160fdd2ef3 (firefox) +0/-38
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedNils Bars
Disclosed2025-03-04

Background

IonMonkey bailout / recover
When the SpiderMonkey JIT (Ion) bails out of optimized code, it reconstructs the interpreter’s expected values from a snapshot. Some MIR instructions are marked can_recover and provide an RInstruction whose recover() re-materializes the result instead of storing it in the snapshot.
`MRegExpMatcher` / `RRegExpMatcher`
The MIR node for the self-hosted RegExp match operation, and its recover counterpart that, during a bailout, re-ran the match to reproduce the result.
`RegExpMatcherRaw`
The C++ routine RRegExpMatcher::recover() invoked to recompute the match; it can execute self-hosted/engine RegExp machinery.
Recover-on-bailout invariant
Recover routines are expected to be side-effect-light reconstructions. Running substantial JS (which can allocate and trigger GC) from inside the bailout path violates the engine’s expectations at that point.

Root Cause Analysis

The patch removes recover-on-bailout support for the RegExp matcher. In js/src/jit/MIROps.yaml the can_recover: true line is deleted from the RegExp-matcher op (and stray can_recover lines on the neighboring RegExp ops are cleaned up). In js/src/jit/Recover.cpp the entire MRegExpMatcher::writeRecoverData and the RRegExpMatcher class (RRegExpMatcher::RRegExpMatcher and RRegExpMatcher::recover) are deleted, and in js/src/jit/Recover.h the RegExpMatcher entry is removed from the recover-instruction list and the RRegExpMatcher class declaration is deleted. The deleted recover() created RootedObject/RootedString from the snapshot and called RegExpMatcherRaw(cx, regexp, input, lastIndex, nullptr, &result).

The root problem: because MRegExpMatcher was recoverable, during a bailout the engine would re-execute the RegExp match via RegExpMatcherRaw. That call runs additional JavaScript/engine code that can allocate and, crucially, trigger garbage collection at a point in the bailout where the engine is not prepared for GC or arbitrary script to run (the advisory: “interrupt the processing of a RegExp bailout and run additional JavaScript, potentially triggering garbage collection when the engine was not expecting it”). By making the matcher non-recoverable, its result is instead captured normally in the snapshot and never recomputed during bailout, eliminating the unexpected reentry/GC. The diff itself only shows the recoverability being withdrawn; it does not show the downstream memory-safety sink, so any use-after-free from an unexpected GC is an inference (not substantiated by this diff). The CVSS vector (C:N/I:N/A:H) is consistent with an engine-state/availability fault rather than a demonstrated info leak or integrity break.

Key insight
Marking MRegExpMatcher recoverable meant a bailout re-ran the match through RegExpMatcherRaw, executing JS and allowing GC in the middle of bailout processing where the engine did not expect it; the fix removes recover-on-bailout for the matcher so its result is snapshotted instead of recomputed.

Attack Path

  1. Attacker script Serves JS that gets the RegExp match path Ion-compiled with MRegExpMatcher marked recoverable (web-reachable; JIT engages on hot code).
  2. Force a bailout Arranges execution so Ion bails out while an MRegExpMatcher result must be recovered (inferred: exact bailout trigger not shown by the diff).
  3. Reentrant match The bailout invokes RRegExpMatcher::recover → RegExpMatcherRaw, running additional JS and allocating mid-bailout.
  4. Unexpected GC That allocation triggers garbage collection at a point the bailout machinery did not anticipate.
  5. Primitive Engine state corruption / potential collection of values the bailout still relies on; the concrete memory-safety consequence (e.g. UAF of an unrooted value) is inferred, not shown by the diff. Substantiated impact is an unexpected-GC engine fault.

Impact Assessment

This is a content-process (renderer) SpiderMonkey JIT soundness bug, reachable by any web page that can run JavaScript, with no privilege and only the interaction of visiting the page (CVSS AV:N, A:H; Mozilla rates it moderate). What the diff substantiates is removal of a recover path that ran JS and permitted GC during bailout — an availability/engine-integrity fault. Escalation to a controllable use-after-free or RCE depends on the unexpected GC freeing a value still in use; that is plausible but inferred and not demonstrated by this diff, so it should not be stated as confirmed RCE.

Changed Functions

FunctionChangeNotes
RRegExpMatcher
js/src/jit/Recover.cpp
modified
RRegExpMatcher
js/src/jit/Recover.h
modified
RStringReplace
js/src/jit/Recover.h
modified

Files Changed

  • js/src/jit/MIROps.yaml
  • js/src/jit/Recover.cpp
  • js/src/jit/Recover.h

Audit Directions

  • Other recoverable ops that call into runnable JS
    Audit remaining can_recover: true MIR ops whose recover() invokes builtins that can allocate or run self-hosted code (potential GC during bailout).
  • GC-safety of recover routines
    Verify recover implementations only reconstruct values without side effects; flag any that call *Raw engine entry points.
  • Bailout reentrancy
    Review whether other self-hosted-backed operations (string replace/split, etc.) can trigger GC or script execution within the snapshot-recover window.
diff --git a/js/src/jit/MIROps.yaml b/js/src/jit/MIROps.yaml
index f9414eab749..a156d535451 100644
--- a/js/src/jit/MIROps.yaml
+++ b/js/src/jit/MIROps.yaml
@@ -1413,7 +1413,6 @@
     lastIndex: Int32
   result_type: Value
   possibly_calls: true
-  can_recover: true
 
 # Note: this instruction writes to cx->regExpSearcherLastLimit.
 # See also MRegExpSearcherLastLimit.
@@ -1424,7 +1423,6 @@
     lastIndex: Int32
   result_type: Int32
   possibly_calls: true
-  can_recover: false
 
 # This instruction loads cx->regExpSearcherLastLimit. We don't have a
 # specialized alias set for this so just use the default alias set similar to
@@ -1439,7 +1437,6 @@
     string: String
   result_type: Value
   possibly_calls: true
-  can_recover: false
 
 - name: RegExpExecTest
   operands:
@@ -1447,7 +1444,6 @@
     string: String
   result_type: Boolean
   possibly_calls: true
-  can_recover: false
 
 - name: RegExpHasCaptureGroups
   operands:
diff --git a/js/src/jit/Recover.cpp b/js/src/jit/Recover.cpp
index e70f5dd50c6..e8a1864ffef 100644
--- a/js/src/jit/Recover.cpp
+++ b/js/src/jit/Recover.cpp
@@ -11,7 +11,6 @@
 #include "jsmath.h"
 
 #include "builtin/Object.h"
-#include "builtin/RegExp.h"
 #include "builtin/String.h"
 #include "jit/AtomicOperations.h"
 #include "jit/Bailouts.h"
@@ -1815,30 +1814,6 @@ bool RNaNToZero::recover(JSContext* cx, SnapshotIterator& iter) const {
   return true;
 }
 
-bool MRegExpMatcher::writeRecoverData(CompactBufferWriter& writer) const {
-  MOZ_ASSERT(canRecoverOnBailout());
-  writer.writeUnsigned(uint32_t(RInstruction::Recover_RegExpMatcher));
-  return true;
-}
-
-RRegExpMatcher::RRegExpMatcher(CompactBufferReader& reader) {}
-
-bool RRegExpMatcher::recover(JSContext* cx, SnapshotIterator& iter) const {
-  RootedObject regexp(cx, iter.readObject());
-  RootedString input(cx, iter.readString());
-
-  // Int32 because |lastIndex| is computed from transpiled self-hosted call.
-  int32_t lastIndex = iter.readInt32();
-
-  RootedValue result(cx);
-  if (!RegExpMatcherRaw(cx, regexp, input, lastIndex, nullptr, &result)) {
-    return false;
-  }
-
-  iter.storeInstructionResult(result);
-  return true;
-}
-
 bool MTypeOf::writeRecoverData(CompactBufferWriter& writer) const {
   MOZ_ASSERT(canRecoverOnBailout());
   writer.writeUnsigned(uint32_t(RInstruction::Recover_TypeOf));
diff --git a/js/src/jit/Recover.h b/js/src/jit/Recover.h
index d73f0ffa9a7..f4622a97f26 100644
--- a/js/src/jit/Recover.h
+++ b/js/src/jit/Recover.h
@@ -124,7 +124,6 @@ namespace jit {
   _(Random)                       \
   _(StringSplit)                  \
   _(NaNToZero)                    \
-  _(RegExpMatcher)                \
   _(StringReplace)                \
   _(Substr)                       \
   _(TypeOf)                       \
@@ -814,14 +813,6 @@ class RNaNToZero final : public RInstruction {
   bool recover(JSContext* cx, SnapshotIterator& iter) const override;
 };
 
-class RRegExpMatcher final : public RInstruction {
- public:
-  RINSTRUCTION_HEADER_NUM_OP_(RegExpMatcher, 3)
-
-  [[nodiscard]] bool recover(JSContext* cx,
-                             SnapshotIterator& iter) const override;
-};
-
 class RStringReplace final : public RInstruction {
  private:
   bool isFlatReplacement_;
Loading diff…