Branch: refs/heads/main
  Home:   https://github.com/WebKit/WebKit
  Commit: 37465a721d7aefafd9cbd775127315e353c05977
      
https://github.com/WebKit/WebKit/commit/37465a721d7aefafd9cbd775127315e353c05977
  Author: Yusuke Suzuki <[email protected]>
  Date:   2026-03-27 (Fri, 27 Mar 2026)

  Changed paths:
    A JSTests/stress/stack-overflow-regexp.js
    A JSTests/stress/yarr-jit-non-greedy-parentheses-backtrack.js
    R LayoutTests/js/script-tests/stack-overflow-regexp.js
    R LayoutTests/js/stack-overflow-regexp-expected.txt
    R LayoutTests/js/stack-overflow-regexp.html
    M Source/JavaScriptCore/yarr/YarrJIT.cpp

  Log Message:
  -----------
  [YARR] Fix JIT crash on non-greedy ParenthesesSubpattern backtracking
https://bugs.webkit.org/show_bug.cgi?id=310771
rdar://173140757

Reviewed by Yijia Huang.

This patch fixes two JIT crashes in YARR's ParenthesesSubpattern handling:

1. Uninitialized returnAddress crash

When a non-greedy multi-alternative ParenthesesSubpattern (e.g. (a|b)*?)
is inside a FixedCount outer group (e.g. (?:...){2}), the returnAddress
frame slot may never be set because the non-greedy path skips the body
entirely — no NestedAlternativeEnd forward code runs. The FixedCount
outer group's saveParenContext then captures this uninitialized value.
During backtracking, NestedAlternativeEnd.bt does loadFromFrameAndJump
on the garbage returnAddress, crashing.

Example: /(?:(a|b)*?.){2}xx/ matching "aabbcc"

    Forward:
      Outer iter 1: (a|b)*? is non-greedy, skips body (0 iterations).
                    returnAddress is never written by NestedAlternativeEnd.
                    '.' matches 'a'. Outer End saves ParenContext
                    (captures returnAddress = UNINITIALIZED).
      Outer iter 2: same, saves another context with UNINITIALIZED.
      "xx" fails at "bb".

    Backtrack:
      Outer Begin.bt restores iter 1's context
        -> returnAddress = UNINITIALIZED is written back to frame.
      Content backtrack: '.' fails, (a|b)*? End.bt re-enters body.
      Body tries alternatives at some position, all fail.
      NestedAlternativeEnd.bt: loadFromFrameAndJump(UNINITIALIZED)
        -> CRASH (SIGBUS on ARM64E, SIGSEGV elsewhere).

Initialize returnAddress to null at ParenthesesSubpatternBegin.
In NestedAlternativeEnd.bt for Greedy/NonGreedy, check for null before
jumping. If null, route directly to Begin.bt's noContext handler via
m_zeroLengthMatch, bypassing intermediate content backtrack handlers
that would operate on stale state.

2. Stale ParenContext chain crash

When a ParenthesesSubpattern (outer) contains nested Greedy/NonGreedy
ParenthesesSubpattern terms (inner), restoreParenContext restores ALL
frame slots including inner patterns' parenContextHead pointers. Those
pointers may reference contexts that were freed by inner backtracking
in a different iteration and subsequently recycled via the free list.
Accessing recycled contexts reads corrupted data (wrong begin/end
indices), causing out-of-bounds input access and SIGSEGV.

This only affects Greedy/NonGreedy inner patterns because their
backtracking explicitly frees contexts (returning them to the free
list). FixedCount inner patterns' contexts are merely made unreachable
(Begin.forward sets parenContextHead=null) but never freed, so they
remain valid when restored.

Example: /((a|b)*?(.)??.){3}cp/ matching "aabbbccc"

    Forward:
      Outer iters 1-3: inner (a|b)*? skips (non-greedy, 0 iterations).
                       inner parenContextHead = null in each saved context.
      "cp" fails.

    Backtrack cycle (iter 3 content retry succeeds):
      Outer Begin.bt restores iter 3's context.
      Content backtrack: (a|b)*? enters, allocates InnerCtx1.
      Match succeeds with different partition. Outer End saves context
        (inner parenContextHead -> InnerCtx1).
      "cp" still fails.

    Backtrack cycle (iter 3 content retry exhausted):
      Outer Begin.bt restores the above context.
      Content backtrack: (a|b)*? tries more iterations, eventually
        exhausts all options. Begin.bt frees InnerCtx1 -> free list.
      Content fully exhausted, falls through to Outer Begin.bt.

    Backtrack cycle (try iter 2):
      Outer Begin.bt restores iter 2's context (from original forward).
      Content backtrack succeeds. Forward re-runs iter 3.
      Outer End saves iter 3's context
        (inner parenContextHead = null, fine).
      "cp" fails. Backtrack into iter 3, then back to iter 2.
      Iter 2 content backtrack: (a|b)*? enters, allocates from free list.
        -> Gets RECYCLED InnerCtx1 (freed above).
      Match succeeds. Outer End saves context
        (inner parenContextHead -> recycled InnerCtx1).
      Forward re-runs iter 3. "cp" fails.
      Outer Begin.bt restores iter 3, exhausts it.
      Outer Begin.bt restores iter 2's context
        (inner parenContextHead -> recycled InnerCtx1).
      But InnerCtx1 was recycled and may have been overwritten
        by a later allocation with different data.
      Inner Begin.bt loads InnerCtx1, restoreParenContext reads
        corrupted begin/end values -> index = 73,000,000
        -> out-of-bounds input access -> SIGSEGV.

After restoreParenContext in both FixedCount and Greedy/NonGreedy
backtracking paths, null out inner Greedy/NonGreedy patterns'
parenContextHead slots. A new helper clearInnerParenContextHeadSlots
walks the pattern tree at JIT compile time to identify affected slots.

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

* JSTests/stress/yarr-jit-non-greedy-parentheses-backtrack.js:
(r5.a.b):
* LayoutTests/js/script-tests/stack-overflow-regexp.js:
(shouldThrow.recursiveCall):
(shouldThrow):
(recursiveCall):
* Source/JavaScriptCore/yarr/YarrJIT.cpp:

Canonical link: https://commits.webkit.org/310115@main



To unsubscribe from these emails, change your notification settings at 
https://github.com/WebKit/WebKit/settings/notifications

Reply via email to