CVE-2025-1934
Overview
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_recoverand provide anRInstructionwhoserecover()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.
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
- Attacker script
Serves JS that gets the RegExp match path Ion-compiled with
MRegExpMatchermarked recoverable (web-reachable; JIT engages on hot code). - Force a bailout
Arranges execution so Ion bails out while an
MRegExpMatcherresult must be recovered (inferred: exact bailout trigger not shown by the diff). - Reentrant match
The bailout invokes
RRegExpMatcher::recover→RegExpMatcherRaw, running additional JS and allocating mid-bailout. - Unexpected GC That allocation triggers garbage collection at a point the bailout machinery did not anticipate.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
RRegExpMatcherjs/src/jit/Recover.cpp |
modified | |
RRegExpMatcherjs/src/jit/Recover.h |
modified | |
RStringReplacejs/src/jit/Recover.h |
modified |
Files Changed
js/src/jit/MIROps.yamljs/src/jit/Recover.cppjs/src/jit/Recover.h
Audit Directions
- Other recoverable ops that call into runnable JSAudit remaining
can_recover: trueMIR ops whoserecover()invokes builtins that can allocate or run self-hosted code (potential GC during bailout). - GC-safety of recover routinesVerify recover implementations only reconstruct values without side effects; flag any that call
*Rawengine entry points. - Bailout reentrancyReview whether other self-hosted-backed operations (string replace/split, etc.) can trigger GC or script execution within the snapshot-recover window.
Patch
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_;