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