← All issues

YARR JIT crash fixes for non-greedy ParenthesesSubpattern backtracking

37465a7

JSTests/stress/yarr-jit-non-greedy-parentheses-backtrack.js

+// Minimal reproducer: non-greedy multi-alt group inside FixedCount.
+// FixedCount inter-iteration backtracking re-enters NestedAlternativeEnd.bt
+// where the uninitialized returnAddress would cause SIGBUS on ARM64E.
+/(?:(a|b)*?.){2}xx/.exec('aabbcc');
+/((a|b)*?[a-c]){3}cp/.exec('aabbbccc');
+/((a|b)*?.){2}cp/.exec('aabbbccc');
+
+// Nested non-greedy inside FixedCount: previously caused SIGSEGV due to stale
+// ParenContext chain after restoreParenContext. Inner Greedy/NonGreedy patterns'
+// parenContextHead must be cleared after restore to prevent freed-context reuse.
+var r5 = /((a|b)*?(.)??.){3}cp/s;
+if (r5.exec('aabbbccc') !== null)
+ throw new Error("Expected null for /((a|b)*?(.)??.){3}cp/s with 'aabbbccc'");
+
+// Complex regex with deeply nested patterns that previously crashed (SIGSEGV).
+var r6 = new RegExp(unescape('%28%28%28%29...'), 'dimsu');
+if (r6.exec('aabbbccc') !== null)
+ throw new Error("Expected null for complex regex with 'aabbbccc'");

This commit fixes two distinct JIT crashes in YARR's non-greedy ParenthesesSubpattern backtracking, both reachable from attacker-controlled regex input. The first is an uninitialized returnAddress frame slot causing SIGBUS on ARM64E (SIGSEGV elsewhere) when a non-greedy multi-alternative group skips its body inside a FixedCount outer loop — the NestedAlternativeEnd.bt handler reads a return address that was never written because the group body was skipped. The fix initializes the returnAddress slot during NestedAlternativeBegin.

The second is more architecturally interesting: a stale ParenContext chain causing out-of-bounds input access. The ParenContext free list recycles released allocations across backtrack iterations. When restoreParenContext restores a snapshot from an earlier outer iteration, it restores the inner pattern's parenContextHead pointer — which now points to a ParenContext that was freed and recycled by a later iteration. The recycled slot contains overwritten data (match indices from a different match path), and reading those indices produces out-of-bounds access into the input string.

  Forward (outer iters 1-3):
    inner (a|b)*? skips → parenContextHead = null saved in each OuterCtx

  Backtrack cycle A (iter 3 retry):
    restoreParenContext(OuterCtx3)
    inner (a|b)*? allocates → InnerCtx1
    match succeeds → OuterCtx3' saved with parenContextHead → InnerCtx1
    next attempt fails → inner backtracks → InnerCtx1 freed → free list

  Backtrack cycle B (try iter 2):
    restoreParenContext(OuterCtx2)    ← original, parenContextHead = null
    inner allocates from free list   ← gets RECYCLED InnerCtx1
    match succeeds → OuterCtx2' saved with parenContextHead → recycled InnerCtx1
    iter 3 re-runs, fails
    restoreParenContext(OuterCtx2')   ← parenContextHead → recycled InnerCtx1
    InnerCtx1 now holds overwritten data (begin/end = 73,000,000)
    inner Begin.bt reads InnerCtx1 → OOB input access → SIGSEGV

The fix adds clearInnerParenContextHeadSlots — after restoreParenContext, all inner Greedy/NonGreedy parenContextHead slots are nulled out, preventing freed-context reuse through stale pointers.

Both bugs are crash-grade and reachable from regex input alone. The stale-context bug is structurally a use-after-free in the ParenContext free list — a freed allocation is recycled, overwritten by a later allocation, and then read via a stale pointer restored by restoreParenContext, with the corrupted read becoming a match index into the input string (OOB read). On non-PAC platforms, the uninitialized returnAddress bug is also worth investigating: whether the JIT stack frame layout can be predicted to place an attacker-influenced value in that slot before the save.

🔒

The recycled-context reuse pattern and uninitialized code-pointer frame slot each have audit directions worth investigating for exploitability beyond crashes.

Subscribe to read more