Yarr JIT: word-boundary assertions corrupt the non-BMP index advance
/\B/u never consumes a character — but it still advanced the match index.
Component: JSC Yarr regex JIT | 116394d
Yarr는 JSC의 regex JIT 컴파일러입니다. Unicode 모드 regex를 처리할 때, 첫 매칭 문자가 surrogate pair로 표현되는 non-BMP 문자인 경우 native code는 match index를 UTF-16 code unit 하나 이상만큼 진행시킬 수 있습니다. 이때 firstCharacterAdditionalReadSize가 그 추가 진행량을 기록해 두었다가, reentry 지점에서 m_regs.index에 다시 더해줌으로써 이후 매칭이 올바른 위치에서 재개되도록 합니다.
문제는 word-boundary assertion(\b/\B)에 있습니다. 이 assertion은 입력을 소비하지 않고 주변 문자를 살펴보기만 하는 zero-width 연산인데, 일반 문자 매칭에 쓰이던 동일한 fast-path 최적화가 이 zero-width peek 코드를 생성하는 동안에도 잘못 활성화된 상태로 남아 있었습니다. 그 결과 일관되지 않은 index 진행값이 생성된 backtrack 및 reentry 코드로 새어 들어가게 됩니다.
패치는 word-boundary assertion 코드를 생성하는 동안 scoped flag를 통해 해당 최적화를 비활성화합니다. 아울러 alternation의 begin/reentry backtrack 경로도 수정되어, 무조건 jump하는 대신 firstCharacterAdditionalReadSize를 m_regs.index에 다시 더하고 checkInput().linkTo(beginOp->m_reentry, ...)를 통해 입력을 재검증하도록 변경되었습니다.
Significance
Index 진행값이 손상되면 malformed JSString read가 발생하며, 이는 /\B|.../u.exec(str) 형태로 사용자가 제어하는 JavaScript에서 직접 도달 가능합니다. Commit message는 이 문제로 관찰되는 결과를 WebContent crash로 설명합니다. 잘못된 match index가 이후 생성 코드에서 그대로 사용되고, 여기서 만들어지는 문자열이 범위를 벗어난 bounds를 기준으로 구성되기 때문입니다.
Audit directions
이 버그는 public tracker에 등록된 전형적인 JIT index 관리 버그이므로, 일회성 사례라기보다는 유사 버그를 찾는 좋은 템플릿에 해당합니다. 좁게 보면, non-consuming term 주변에서 firstCharacterAdditionalReadSize와 m_regs.index를 읽거나 변경하는 모든 지점을 점검할 필요가 있습니다. Lookahead, lookbehind 등 다른 zero-width 구성 요소가 m_useFirstNonBMPCharacterOptimization을 제대로 scoping하지 않은 채 재사용하고 있다면 같은 계열의 버그를 안고 있을 가능성이 있습니다. 리뷰에서 눈여겨봐야 할 신호는, 입력을 소비하지 않는 term의 코드 생성기 내부에서 이 최적화 flag가 참조되고 있는지입니다.
조금 더 넓게 보면, 특정 term 종류에만 유효한 bookkeeping 상태를 JIT가 유지하면서도 그 flag는 컴파일 전체에 걸쳐 process-wide로 적용되는 패턴이 다른 곳에서도 반복될 수 있습니다. Index 복원이 수작업으로 작성되어 있고 자동으로 도출되지 않는 모든 alternation 및 repetition 구성의 backtrack·reentry 경로를 점검할 필요가 있습니다.
가장 넓은 범위에서는, surrogate pair와 backtracking reentry 지점을 포함한 lookaround·assertion 조합의 Unicode regex를 fuzzing해보는 것이 유효합니다. 특히 흥미로운 corpus는 zero-width term이 alternation의 첫 번째 분기로 오는 입력입니다. 이 경우 first-character 최적화는 활성화되지만 zero-width term은 이를 해제하지 않기 때문입니다.