← All issues

YARR JIT crash fixes for non-greedy ParenthesesSubpattern backtracking

37465a7

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

+// 최소 재현 조건: FixedCount 내부에 위치한 non-greedy multi-alt 그룹.
+// FixedCount 반복 간 backtracking이 NestedAlternativeEnd.bt로 재진입하는 시점에
+// 초기화되지 않은 returnAddress가 ARM64E에서 SIGBUS를 유발합니다.
+/(?:(a|b)*?.){2}xx/.exec('aabbcc');
+/((a|b)*?[a-c]){3}cp/.exec('aabbbccc');
+/((a|b)*?.){2}cp/.exec('aabbbccc');
+
+// FixedCount 내부의 중첩된 non-greedy: restoreParenContext 이후 stale
+// ParenContext chain으로 인해 SIGSEGV가 발생했습니다. restore 이후
+// 내부 Greedy/NonGreedy 패턴의 parenContextHead를 반드시 초기화해야
+// 해제된 context의 재사용을 방지할 수 있습니다.
+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'");
+
+// 깊게 중첩된 패턴이 포함된 복잡한 regex — 이전에 SIGSEGV crash가 발생했습니다.
+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'");

이 commit은 YARR의 non-greedy ParenthesesSubpattern backtracking에서 발생하는 두 가지 JIT crash를 수정합니다. 두 버그 모두 attacker가 제어하는 regex 입력만으로 도달할 수 있습니다.

첫 번째는 초기화되지 않은 returnAddress frame slot 문제입니다. non-greedy multi-alternative 그룹이 FixedCount 외부 루프 내에서 본체를 건너뛰는 경우, NestedAlternativeEnd.bt 핸들러가 한 번도 기록된 적 없는 return address를 읽게 됩니다. ARM64E에서는 SIGBUS가, 다른 플랫폼에서는 SIGSEGV가 발생합니다. 수정 방법은 NestedAlternativeBegin 단계에서 returnAddress slot을 미리 초기화하는 것입니다.

두 번째 버그는 구조적으로 더 흥미롭습니다. stale ParenContext chain이 입력 문자열에 대한 out-of-bounds access를 유발합니다. ParenContext free list는 backtrack 반복 간에 해제된 allocation을 재사용합니다. restoreParenContext가 이전 외부 반복의 snapshot을 복원할 때, 내부 패턴의 parenContextHead 포인터도 함께 복원됩니다. 문제는 이 포인터가 이미 해제되어 이후 반복에서 재사용된 ParenContext를 가리킨다는 점입니다. 재사용된 slot에는 다른 match 경로에서 덮어쓴 데이터(match index)가 들어 있으며, 이 index를 읽으면 입력 문자열 범위를 벗어난 접근이 발생합니다.

  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

수정 사항으로 clearInnerParenContextHeadSlots가 추가되었습니다. restoreParenContext 이후 내부의 모든 Greedy/NonGreedy parenContextHead slot을 null로 초기화합니다. 이를 통해 stale 포인터를 통한 해제된 context의 재사용을 방지합니다.

두 버그 모두 crash-grade에 해당하며, regex 입력만으로 도달 가능합니다. stale context 버그는 구조적으로 ParenContext free list에서의 use-after-free입니다. 해제된 allocation이 재사용되고 이후 allocation에 의해 덮어써진 뒤, restoreParenContext가 복원한 stale 포인터를 통해 읽힙니다. 이때 오염된 데이터가 입력 문자열에 대한 match index로 사용되어 OOB read가 발생합니다. PAC가 적용되지 않은 플랫폼에서는 초기화되지 않은 returnAddress 버그도 살펴볼 필요가 있습니다. JIT stack frame 레이아웃을 예측하여 저장 이전 단계에서 attacker가 영향을 미친 값을 해당 slot에 위치시키는 것이 가능한지 확인해야 합니다.

🔒

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

더 확인하려면 구독해 주세요