Skip to content

Fix a Safari crash in the selectionchange handler - #21778

Open
taneli-linear wants to merge 1 commit into
mozilla:masterfrom
taneli-linear:selectionchange-null-anchor
Open

Fix a Safari crash in the selectionchange handler#21778
taneli-linear wants to merge 1 commit into
mozilla:masterfrom
taneli-linear:selectionchange-null-anchor

Conversation

@taneli-linear

Copy link
Copy Markdown

The problem

Safari throws an unhandled TypeError in the global selectionchange listener of TextLayerBuilder:

TypeError: null is not an object (evaluating 'anchor.previousSibling')

The listener searches for the node that comes before the end of the selection
(web/text_layer_builder.js, in #enableGlobalSelectionListener).
The search moves the anchor variable up with parentNode and back with previousSibling.
The search has no null check.

Only Safari comes to this code:

The crash is frequent. The error telemetry of our application shows more than 1500 events
from 143 users in four months, across Safari 16 through Safari 26. All events come from
Safari on macOS. Our session data shows a common sequence: the user closes a modal PDF
preview, continues to use the page, and the next selectionchange event throws.

The solution

Add a null guard to the three expressions that read anchor:

       if (!modifyStart && range.endOffset === 0) {
         do {
-          while (!anchor.previousSibling) {
+          while (anchor && !anchor.previousSibling) {
             anchor = anchor.parentNode;
           }
-          anchor = anchor.previousSibling;
-        } while (!anchor.childNodes.length);
+          anchor = anchor?.previousSibling;
+        } while (anchor && !anchor.childNodes.length);
       }

-      const parentTextLayer = anchor.parentElement?.closest(".textLayer");
+      const parentTextLayer = anchor?.parentElement?.closest(".textLayer");

Tests

To see the crash, make the boundary shape directly. In Safari on macOS:

  1. Open https://mozilla.github.io/pdf.js/web/viewer.html (or any PDF in the viewer).
  2. Open the Web Inspector console.
  3. Run this code:
const r = document.createRange();
r.setStart(document.querySelector(".textLayer"), 0);
r.setEnd(document.documentElement, 0);
getSelection().removeAllRanges();
getSelection().addRange(r);

Without this change, the console shows the TypeError. This reproduces on the live demo
viewer (pdf.js 6.3.175 at the time of this report) in Safari.
With this change, no error occurs.

Note: the exact user gesture that makes Safari report this boundary shape is version
dependent. Our telemetry shows the events on Safari 16 through Safari 26 in normal use,
but recent Safari versions report the shape less often. The code above makes the same
boundary shape that Safari makes, and gives a stable reproduction on all versions.

Safari can report a selection boundary whose backwards walk leaves the
document (range end on <html>/<head> with endOffset === 0, or a boundary
in a disconnected subtree). The walk then reads previousSibling of a null
parentNode and throws
  TypeError: null is not an object (evaluating 'anchor.previousSibling')
in the selectionchange listener. Guard the three reads of anchor; when the
walk escapes the document, parentTextLayer is undefined and the
endOfContent handling is skipped, which is correct because the selection
end is not in a text layer.
@taneli-linear
taneli-linear marked this pull request as ready for review August 14, 2026 13:02
@taneli-linear taneli-linear changed the title Add null guards to the selectionchange anchor search in TextLayerBuilder (fixes a Safari crash) Fix a Safari crash in the selectionchange handler Aug 14, 2026

@nicolo-ribaudo nicolo-ribaudo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When does anchor become null? Is there a node with no .parentNode?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants