LibWeb: Avoid a crash in Selection.containsNode() with a detached range

Problem: Selection.containsNode() crashes when the selection’s range
has boundary points in a tree that’s not connected — such as a shadow
tree whose host was removed. We also call that function internally for
content-visibility:auto elements during rendering — so it can cause a
page to crash even when the page itself doesn’t use the Selection API.

Cause: When a shadow host is removed, a selection range whose boundary
points are inside its shadow tree isn’t adjusted: live-range adjustment
matches ranges by regular-tree descendant and doesn’t reach into shadow
trees. The range is left pointing into the detached shadow tree.
contains_node() verifies only the passed node against the document — not
the range — then runs a boundary-point comparison that requires both
sides to share a shadow-including root.

Fix: Make contains_node() also confirm the range’s boundary points are
rooted at the document. If not, then return false — because a node can’t
be contained in a selection whose range is in a different tree.

Fixes https://github.com/LadybirdBrowser/ladybird/issues/9469
This commit is contained in:
sideshowbarker 2026-05-22 23:05:48 +09:00 committed by Andreas Kling
parent 2153535057
commit 4696a1ed97
2 changed files with 18 additions and 0 deletions

View file

@ -482,6 +482,10 @@ bool Selection::contains_node(GC::Ref<DOM::Node> node, bool allow_partial_contai
// The method must return false if this is empty or if node's root is not the document associated with this.
if (!m_range)
return false;
// The range's boundary points can be in a tree that's not connected to the document (for example, inside the shadow
// tree of a removed host). Such a range isn't comparable with a node in the document.
if (&m_range->start().node->shadow_including_root() != m_document.ptr())
return false;
if (&node->root() != m_document.ptr())
return false;

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<!-- Regression test for https://github.com/LadybirdBrowser/ladybird/issues/9469
Removing a shadow host leaves a selection range inside its shadow tree disconnected
from the document. Selection.containsNode() must not crash on such a range. -->
<div id="host"></div>
<script>
const host = document.getElementById("host");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = "<span>shadow text</span>";
const text = shadow.querySelector("span").firstChild;
getSelection().setBaseAndExtent(text, 0, text, 4);
host.remove();
getSelection().containsNode(document.body, true);
</script>