High firefox OOB 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionIncorrect boundary conditions in the JavaScript Engine component
ComponentSpiderMonkey
Bug ClassOOB
Tracker2078727
Fix commit59579da43a4e (firefox) +1/-1
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

Lazy evaluation
To avoid recomputing match details, RegExpStatics can be left “lazy”: it records just enough state (the source regexp and input) and defers producing concrete capture boundaries until something reads them. executeLazy(JSContext*) materializes those results on demand by re-running the regexp.
`RegExpRunStatus`
The result of running a regexp: Success (matched, with valid capture bounds) or a non-success status (no match / error). Capture boundary indices are only meaningful on Success.
`MOZ_ASSERT` vs `MOZ_RELEASE_ASSERT`
MOZ_ASSERT is compiled out of release/opt builds; MOZ_RELEASE_ASSERT is retained in all builds and aborts the process if the condition fails.

Root Cause Analysis

RegExpStatics::executeLazy re-runs the stored regexp against the stored input to reconstruct match details, relying on the documented invariant that “RegExpStatics are only updated on successful (matching) execution. Re-running the same expression must therefore produce a matching result.” The code asserted this with MOZ_ASSERT(status == RegExpRunStatus::Success). Because MOZ_ASSERT is removed in release builds, in shipping Firefox this check did nothing: if the lazy re-execution did not return Success (the invariant broken — e.g. a case where re-running the same source/input fails to match, so status != Success), execution fell straight through. It then proceeded to “unset lazy state” and populate the match/capture boundary fields from a run that had no valid match, so the subsequently exposed match span and capture indices derive from uninitialized/stale boundary data. Consuming those bounds to slice the subject string yields an out-of-bounds read of string memory — the “incorrect boundary conditions” the advisory names.

The one-line patch upgrades the check to MOZ_RELEASE_ASSERT(status == RegExpRunStatus::Success), so the invariant is now enforced in all builds: if a lazy re-execution ever fails to match, the process aborts deterministically instead of continuing with invalid capture boundaries. The diff shows the invariant and its newly added runtime enforcement; the downstream code that would have consumed the bad boundaries is below the patched line and is not shown by the diff, so the precise OOB sink is (inferred) from the comment and the advisory’s “incorrect boundary conditions” classification. The fix is a hardening-to-crash of a previously-unchecked boundary invariant rather than a repair of the match logic itself.

Key insight
The guarantee that a lazy RegExpStatics re-execution always matches was enforced only by a debug-only MOZ_ASSERT; in release builds a non-matching re-run slipped through and populated capture-boundary fields from a failed match, so later use of those indices read out of bounds — the fix makes the invariant a release assertion.

Attack Path

  1. Create a lazy RegExpStatics state Script performs a successful regexp match that leaves the statics in the lazy (deferred) state for the realm.
  2. Reach `executeLazy` Access a legacy RegExp static (e.g. RegExp.lastMatch/$1) or another consumer that forces executeLazy to re-run the stored regexp to materialize boundaries.
  3. Break the re-match invariant Arrange conditions (inferred) under which re-running the stored source against the stored input does not return Success, so status != RegExpRunStatus::Success.
  4. Fall through the elided check In a release build the removed MOZ_ASSERT does nothing; executeLazy unsets lazy state and fills in match span / capture boundaries from the failed run.
  5. Primitive Reading the resulting match boundaries slices the subject string with invalid start/limit indices, producing an out-of-bounds read of adjacent memory (information disclosure); on the patched build the same path instead aborts via MOZ_RELEASE_ASSERT.

Impact Assessment

The code runs in the content (renderer) process and is reachable from unprivileged JavaScript through ordinary RegExp usage and the legacy static properties that trigger lazy materialization. In unpatched release builds the realized primitive is use of invalid regexp capture/match boundary indices, i.e. an out-of-bounds read of string/heap memory that can leak data back to script (information disclosure). The precondition — forcing a lazy re-execution to fail to match despite the same source and input — is not demonstrated by the diff and is (inferred); it is the hard part of weaponization. Note the patch’s effect is to convert the condition into a deterministic crash, so the fix trades a potential memory-safety read for a controlled abort rather than correcting the match path, which bounds the pre-fix impact to a read/DoS primitive as substantiated here.

Files Changed

  • js/src/vm/RegExpStatics.cpp

Audit Directions

  • Debug-only guards on security invariants
    Grep SpiderMonkey for MOZ_ASSERT checks that gate the validity of lengths, indices, or boundaries that are then used to index memory in release builds; these are candidates for the same compiled-out-check class of bug.
  • Lazy regexp result consumers
    Review the code immediately after this assertion and all readers of RegExpStatics capture/match-span fields (the legacy $1…$9, lastMatch, lastParen, pending input) to confirm they cannot act on bounds from a non-Success run.
  • Lazy-materialization patterns elsewhere
    Look for other “defer then recompute, assuming the recompute agrees” designs (JIT, parser, intl) where a mismatch between the first and second computation is only debug-asserted.
  • `executeLazy` failure handling
    Determine whether a legitimate way exists for the re-execution to fail (GC, interrupt, changed static state), which would indicate a deeper logic fix is still warranted beyond the release assertion.
diff --git a/js/src/vm/RegExpStatics.cpp b/js/src/vm/RegExpStatics.cpp
index e4b7c4b813b..be912751877 100644
--- a/js/src/vm/RegExpStatics.cpp
+++ b/js/src/vm/RegExpStatics.cpp
@@ -48,7 +48,7 @@ bool RegExpStatics::executeLazy(JSContext* cx) {
    * RegExpStatics are only updated on successful (matching) execution.
    * Re-running the same expression must therefore produce a matching result.
    */
-  MOZ_ASSERT(status == RegExpRunStatus::Success);
+  MOZ_RELEASE_ASSERT(status == RegExpRunStatus::Success);
 
   /* Unset lazy state and remove rooted values that now have no use. */
   pendingLazyEvaluation = false;
Loading diff…