← All issues

::first-letter Selection Fix: New rendererAndOffset() Position Resolver

1f01e40

Source/WebCore/dom/Position.cpp

+std::pair<RenderObject*, int> Position::rendererAndOffset() const
+{
+ Node* node = deprecatedNode();
+ if (!node)
+ return { nullptr, 0 };
+ auto* renderer = node->renderer();
+ if (!renderer)
+ return { nullptr, 0 };
+ int offset = deprecatedEditingOffset();
+ if (auto* fragment = dynamicDowncast<RenderTextFragment>(*renderer)) {
+ if (auto* firstLetterRenderer = fragment->firstLetterRenderer()) {
+ if (offset < fragment->start())
+ return { firstLetterRenderer, offset };
+ }
+ return { fragment, offset - fragment->start() };
+ }
+ return { renderer, offset };
+}

이 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 과정에서 하나로 합쳐지는 것을 방지합니다.

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::handleFirstLetterfirstLetterRenderer에 대한 null 검사를 명시적으로 추가했습니다. "backwards iterator가 text node를 참조하는 도중 DOM mutation으로 first-letter renderer가 소멸될 수 있다"는 이유에서입니다. 이는 live UAF 패턴에 해당합니다. 마지막으로, 전체 텍스트가 한 글자인 경우(first-letter edge case)에는 RenderTextFragment가 비어 있는 상태가 됩니다. 이때 offset routing에서 off-by-one이 발생하면 잘못된 renderer로 라우팅될 수 있는 degenerate 케이스가 만들어집니다.

🔒

New offset routing logic across multiple editing subsystems — edge cases in the first-letter split and mutation-during-iteration patterns are worth investigating.

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