LibWeb: Fix a crash when selecting across an element with no layout box

Problem: Crash when dragging a text selection across an element with no
layout box (e.g., a display:contents element).

Cause: set_user_selection() looks for a user-select:contain ancestor by
walking up the tree via two while-loop conditions that called
layout_node()->user_select_used_value() for each element. But elements
without layout boxes have no layout nodes. So that can dereference null.

Fix: Check layout_node() in the tree-walking while conditions.

Fixes: https://github.com/LadybirdBrowser/ladybird/issues/10062
This commit is contained in:
sideshowbarker 2026-06-14 20:46:48 +09:00 committed by Andreas Kling
parent 67030ceead
commit c2db5c0dcb
3 changed files with 28 additions and 2 deletions

View file

@ -1836,7 +1836,7 @@ static void set_user_selection(GC::Ptr<DOM::Node> anchor_node, size_t anchor_off
// focus node, as this means they are inside the same contain element, or not in a contain element at all.
// This takes care of the "selection trying to escape from a contain" case.
while (
(!potential_contain_node->is_element() || potential_contain_node->layout_node()->user_select_used_value() != CSS::UserSelect::Contain) && potential_contain_node->parent() && !potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
(!potential_contain_node->is_element() || !potential_contain_node->layout_node() || potential_contain_node->layout_node()->user_select_used_value() != CSS::UserSelect::Contain) && potential_contain_node->parent() && !potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
potential_contain_node = potential_contain_node->parent();
}
@ -1861,7 +1861,7 @@ static void set_user_selection(GC::Ptr<DOM::Node> anchor_node, size_t anchor_off
auto target_node = potential_contain_node;
potential_contain_node = focus_node;
while (
(!potential_contain_node->is_element() || potential_contain_node->layout_node()->user_select_used_value() != CSS::UserSelect::Contain) && potential_contain_node->parent() && potential_contain_node != target_node) {
(!potential_contain_node->is_element() || !potential_contain_node->layout_node() || potential_contain_node->layout_node()->user_select_used_value() != CSS::UserSelect::Contain) && potential_contain_node->parent() && potential_contain_node != target_node) {
potential_contain_node = potential_contain_node->parent();
}
if (

View file

@ -0,0 +1 @@
selection includes alpha: true

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<script src="include.js"></script>
<body>
<div style="display: contents">
<p>alpha</p>
<p>bravo</p>
</div>
<script>
test(() => {
document.body.offsetWidth;
const paragraphs = document.querySelectorAll("p");
const first = paragraphs[0].getBoundingClientRect();
const second = paragraphs[1].getBoundingClientRect();
// Drag a selection from the first paragraph into the second. Both paragraphs are children of a display:contents
// element, which has no layout box. So, the user-select:contain containment walk steps onto an element whose
// layout_node() is null, and must not dereference it.
internals.mouseDown(first.left + 1, first.top + first.height / 2);
internals.mouseMove(second.right - 1, second.top + second.height / 2);
internals.mouseUp(second.right - 1, second.top + second.height / 2);
println(`selection includes alpha: ${getSelection().toString().includes("alpha")}`);
});
</script>