LibWeb: Fix crash removing link stylesheet nested in a shadow tree

HTMLLinkElement::removed_from() used `old_root` to find the
StyleSheetList to remove the link's stylesheet from. That's wrong
when the link element lives inside a shadow tree that is itself
nested within a larger removed subtree: Node::remove() hands every
shadow-including descendant the outer subtree's root as `old_root`,
not the descendant's own containing root. So we'd look in the
document's list while the sheet was actually in the shadow root's
list, failing the did_remove VERIFY in StyleSheetList::remove_sheet.

Fix by using the sheet's own owning-root tracking. A link-owned sheet
always has exactly one owning document or shadow root (only constructed
stylesheets can be adopted, and link sheets are never constructed), so
we can just read that entry.

Also make owning_documents_or_shadow_roots() return by const reference
instead of copying the HashTable on every call, which benefits existing
iterating callers too.

Fixes a crash on https://nytimes.com/.
This commit is contained in:
Andreas Kling
2026-04-23 17:47:53 +02:00
committed by Andreas Kling
parent bd4ef4b95a
commit 3cf24872c4
Notes: github-actions[bot] 2026-04-23 20:38:05 +00:00
3 changed files with 27 additions and 5 deletions

View File

@@ -90,7 +90,7 @@ public:
void for_each_effective_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const;
void for_each_effective_counter_style_at_rule(Function<void(CSSCounterStyleRule const&)> const& callback) const;
HashTable<GC::Ptr<DOM::Node>> owning_documents_or_shadow_roots() const { return m_owning_documents_or_shadow_roots; }
HashTable<GC::Ptr<DOM::Node>> const& owning_documents_or_shadow_roots() const { return m_owning_documents_or_shadow_roots; }
void add_owning_document_or_shadow_root(DOM::Node& document_or_shadow_root);
void remove_owning_document_or_shadow_root(DOM::Node& document_or_shadow_root);
void invalidate_owners(DOM::StyleInvalidationReason, ShadowRootStylesheetEffects const* previous_sheet_effects = nullptr);

View File

@@ -85,11 +85,18 @@ void HTMLLinkElement::removed_from(IsSubtreeRoot is_subtree_root, Node* old_ance
Base::removed_from(is_subtree_root, old_ancestor, old_root);
if (m_loaded_style_sheet) {
auto& style_sheet_list = [&old_root] -> CSS::StyleSheetList& {
if (auto* shadow_root = as_if<DOM::ShadowRoot>(old_root); shadow_root)
// NB: We can't use `old_root` here. When this link element is nested
// inside a shadow tree within a larger removed subtree, `old_root`
// is the outer subtree's root, not the shadow root that actually
// contains our stylesheet. Use the sheet's own tracked owning
// root, which is the one whose StyleSheetList it belongs to.
auto const& owning_roots = m_loaded_style_sheet->owning_documents_or_shadow_roots();
VERIFY(owning_roots.size() == 1);
auto& owning_root = **owning_roots.begin();
auto& style_sheet_list = [&owning_root] -> CSS::StyleSheetList& {
if (auto* shadow_root = as_if<DOM::ShadowRoot>(owning_root))
return shadow_root->style_sheets();
return as<DOM::Document>(old_root).style_sheets();
return as<DOM::Document>(owning_root).style_sheets();
}();
style_sheet_list.remove_a_css_style_sheet(*m_loaded_style_sheet);

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<div id="container">
<div id="host"></div>
</div>
<script>
const host = document.getElementById('host');
const shadow = host.attachShadow({ mode: 'open' });
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'data:text/css,body{color:red}';
link.onload = () => {
document.getElementById('container').remove();
};
shadow.appendChild(link);
</script>