::first-letter Selection Fix: New rendererAndOffset() Position Resolver
Source/WebCore/dom/Position.cpp
이 commit은 약 20년 된 버그를 수정합니다. ::first-letter 스타일이 적용된 문자를 선택할 수 없고, caret 위치 지정이 실패하는 문제였습니다. WebKit의 ::first-letter 구현은 하나의 DOM text node를 두 개의 renderer로 분리합니다. 첫 번째 문자는 RenderInline(::first-letter) pseudo-element 하위의 익명 RenderText로, 나머지는 RenderTextFragment로 처리됩니다. 기존의 모든 DOM offset 연산은 node와 renderer가 1:1로 대응한다고 가정하고 있었기 때문에, 항상 잘못된 renderer를 참조하는 문제가 있었습니다. 이를 해결하기 위해 Position::rendererAndOffset()이 도입되었습니다. 이 함수는 임의의 DOM position에 대해 올바른 renderer와 fragment-local offset을 결정하는 중심 helper 역할을 합니다.
DOM: RenderTree:
TextNode "Hello" RenderBlock (div)
offset 0 = 'H' ──► RenderInline (::first-letter)
offset 1 = 'e' RenderText "H" (local offset 0)
offset 2 = 'l' ──► RenderTextFragment "ello" (local offset 0-3)
...
Before fix: After fix:
node->renderer() Position::rendererAndOffset()
always returns offset 0 → RenderText "H", local 0
RenderTextFragment offset 1 → RenderTextFragment, local 0
(misses offset 0) offset 2 → RenderTextFragment, local 1
이 변경은 10개 이상의 소스 파일에 걸쳐 있습니다. caret 배치, selection painting, TextIterator, BiDi 경계 조정, upstream/downstream canonicalization이 모두 영향을 받습니다. 새로 추가된 crossesFirstLetterBoundary 함수는 first-letter 경계를 사이에 둔 서로 다른 caret position이 VisiblePosition canonicalization 과정에서 하나로 합쳐지는 것을 방지합니다.
Significance
DOM position/selection 서브시스템 전반에 걸친 광범위한 refactor입니다. DOM offset을 inline box로 변환하는 모든 호출 지점에 first-letter 분리에 관한 새로운 invariant가 적용되며, off-by-one, use-after-free, state confusion 버그에 대한 실질적인 attack surface가 형성됩니다.
rendererAndOffset() helper는 offset - fragment->start() 연산으로 DOM offset을 fragment-local offset으로 변환합니다. 이 결과를 역변환 없이 DOM 수준 연산에 다시 전달하는 호출 지점이 있다면, 잘못된 offset이 조용히 사용될 수 있습니다. 한편 commit은 SimplifiedBackwardsTextIterator::handleFirstLetter에 firstLetterRenderer에 대한 null 검사를 명시적으로 추가했습니다. "backwards iterator가 text node를 참조하는 도중 DOM mutation으로 first-letter renderer가 소멸될 수 있다"는 이유에서입니다. 이는 live UAF 패턴에 해당합니다. 마지막으로, 전체 텍스트가 한 글자인 경우(first-letter edge case)에는 RenderTextFragment가 비어 있는 상태가 됩니다. 이때 offset routing에서 off-by-one이 발생하면 잘못된 renderer로 라우팅될 수 있는 degenerate 케이스가 만들어집니다.