::first-letter Selection Fix: New rendererAndOffset() Position Resolver
Source/WebCore/dom/Position.cpp
This fixes a 20-year-old bug where ::first-letter styled characters were unselectable and caret positioning failed. WebKit's ::first-letter implementation splits one DOM text node into two renderers: an anonymous RenderText for the first letter under a RenderInline(::first-letter) pseudo-element, and a RenderTextFragment for the remainder. Every DOM offset operation assumed a 1:1 node-to-renderer mapping and always consulted the wrong renderer. The fix introduces Position::rendererAndOffset() — a central helper that resolves the correct renderer and fragment-local offset for any DOM position.
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
The change threads through 10+ source files: caret placement, selection painting, TextIterator, BiDi boundary adjustment, and upstream/downstream canonicalization. A new crossesFirstLetterBoundary function prevents VisiblePosition canonicalization from collapsing distinct caret positions across the first-letter split.
Significance
This is a wide-ranging refactor of the DOM position/selection subsystem — every call site that resolves DOM offsets to inline boxes now carries new invariants around the first-letter split, creating meaningful attack surface for off-by-one, use-after-free, and state confusion bugs.
The rendererAndOffset() helper performs offset - fragment->start() to convert DOM offsets to fragment-local offsets. Any call site that passes the result back to a DOM-level operation without reconversion would silently use a wrong offset. The commit explicitly adds a null-check for firstLetterRenderer in SimplifiedBackwardsTextIterator::handleFirstLetter because "the first-letter renderer can be destroyed by DOM mutations while the backwards iterator is still referencing the text node" — a live UAF pattern. The single-character first-letter edge case (entire text is one character, RenderTextFragment is empty) creates a degenerate case where off-by-one in offset routing would misroute to the wrong renderer.