ladybird/Libraries/LibWeb/DOM/HTMLCollection.cpp
sideshowbarker efa9388adc LibWeb: Fix use-after-free in live-collection filter captures
Problem: Holding form.elements while the form is detached + dropped hit
a use-after-free: the form is GC’ed while the collection’s still live.

Cause: HTMLCollection (and LiveNodeList too) was storing its filter as
an AK::Function — which the garbage collector doesn’t visit. When a
filter lambda captures a GC object (e.g. the form in form.elements) that
object has no GC edge keeping it alive. So it can be collected while the
collection using it’s still reachable — leaving a dangling pointer.

Fix: HTMLCollection and LiveNodeList are GC cells with their own
visit_edges. So, visit the filter’s (and sort’s) capture range there:
conservatively mark any GC object a captured lambda holds — to ensure
it’s kept alive as long as the collection’s reachable.

Fixes https://github.com/LadybirdBrowser/ladybird/issues/9948
2026-06-07 22:16:33 +02:00

208 lines
7.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2021-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/InsertionSort.h>
#include <LibWeb/Bindings/HTMLCollection.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/HTMLCollection.h>
#include <LibWeb/DOM/ParentNode.h>
#include <LibWeb/Namespace.h>
namespace Web::DOM {
GC_DEFINE_ALLOCATOR(HTMLCollection);
GC::Ref<HTMLCollection> HTMLCollection::create(ParentNode& root, Scope scope, Function<bool(Element const&)> filter, Function<bool(Element const&, Element const&)> sort)
{
return root.realm().create<HTMLCollection>(root, scope, move(filter), move(sort));
}
HTMLCollection::HTMLCollection(ParentNode& root, Scope scope, Function<bool(Element const&)> filter, Function<bool(Element const&, Element const&)> sort)
: PlatformObject(root.realm())
, GC::WeakContainer(heap())
, m_root(root)
, m_filter(move(filter))
, m_sort(move(sort))
, m_scope(scope)
{
m_legacy_platform_object_flags = LegacyPlatformObjectFlags {
.supports_indexed_properties = true,
.supports_named_properties = true,
.has_legacy_unenumerable_named_properties_interface_extended_attribute = true,
};
}
HTMLCollection::~HTMLCollection() = default;
void HTMLCollection::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLCollection);
Base::initialize(realm);
}
void HTMLCollection::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_root);
visitor.visit_possible_values(m_filter.raw_capture_range());
visitor.visit_possible_values(m_sort.raw_capture_range());
}
GC::Cell const& HTMLCollection::owner_cell(Badge<GC::Heap>) const
{
return *this;
}
void HTMLCollection::remove_dead_cells(Badge<GC::Heap>)
{
m_cached_elements.remove_all_matching([&](GC::RawPtr<Element> const& element) {
auto* block = GC::HeapBlock::from_cell(element);
return !heap().is_live_heap_block(block) || element->state() != Cell::State::Live || !element->is_marked();
});
if (m_cached_name_to_element_mappings) {
m_cached_name_to_element_mappings->remove_all_matching([&](FlyString const&, GC::RawPtr<Element> const& element) {
auto* block = GC::HeapBlock::from_cell(element);
return !heap().is_live_heap_block(block) || element->state() != Cell::State::Live || !element->is_marked();
});
}
}
void HTMLCollection::update_name_to_element_mappings_if_needed() const
{
update_cache_if_needed();
if (m_cached_name_to_element_mappings)
return;
m_cached_name_to_element_mappings = make<OrderedHashMap<FlyString, GC::RawPtr<Element>>>();
for (auto const& element : m_cached_elements) {
// 1. If element has an ID which is not in result, append elements ID to result.
if (auto const& id = element->id(); id.has_value()) {
if (!id.value().is_empty() && !m_cached_name_to_element_mappings->contains(id.value()))
m_cached_name_to_element_mappings->set(id.value(), element);
}
// 2. If element is in the HTML namespace and has a name attribute whose value is neither the empty string nor is in result, append elements name attribute value to result.
if (element->namespace_uri() == Namespace::HTML && element->name().has_value()) {
auto element_name = element->name().value();
if (!element_name.is_empty() && !m_cached_name_to_element_mappings->contains(element_name))
m_cached_name_to_element_mappings->set(move(element_name), element);
}
}
}
void HTMLCollection::update_cache_if_needed() const
{
// Nothing to do, the DOM hasn't updated since we last built the cache.
if (m_cached_dom_tree_version == root()->document().dom_tree_version())
return;
m_cached_elements.clear();
m_cached_name_to_element_mappings = nullptr;
if (m_scope == Scope::Descendants) {
m_root->for_each_in_subtree_of_type<Element>([&](auto& element) {
if (m_filter(element))
m_cached_elements.append(element);
return TraversalDecision::Continue;
});
} else {
m_root->for_each_child_of_type<Element>([&](auto& element) {
if (m_filter(element))
m_cached_elements.append(element);
return IterationDecision::Continue;
});
}
if (m_sort) {
insertion_sort(m_cached_elements, [this](auto const& a, auto const& b) {
return this->m_sort(*a, *b);
});
}
m_cached_dom_tree_version = root()->document().dom_tree_version();
}
GC::RootVector<GC::Ref<Element>> HTMLCollection::collect_matching_elements() const
{
update_cache_if_needed();
GC::RootVector<GC::Ref<Element>> elements;
for (auto& element : m_cached_elements)
elements.append(*element);
return elements;
}
// https://dom.spec.whatwg.org/#dom-htmlcollection-length
size_t HTMLCollection::length() const
{
// The length getter steps are to return the number of nodes represented by the collection.
update_cache_if_needed();
return m_cached_elements.size();
}
// https://dom.spec.whatwg.org/#dom-htmlcollection-item
Element* HTMLCollection::item(size_t index) const
{
// The item(index) method steps are to return the indexth element in the collection. If there is no indexth element in the collection, then the method must return null.
update_cache_if_needed();
if (index >= m_cached_elements.size())
return nullptr;
return m_cached_elements[index];
}
// https://dom.spec.whatwg.org/#dom-htmlcollection-nameditem-key
Element* HTMLCollection::named_item(FlyString const& key) const
{
// 1. If key is the empty string, return null.
if (key.is_empty())
return nullptr;
update_name_to_element_mappings_if_needed();
if (auto it = m_cached_name_to_element_mappings->get(key); it.has_value())
return it.value();
return nullptr;
}
// https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-names
bool HTMLCollection::is_supported_property_name(FlyString const& name) const
{
update_name_to_element_mappings_if_needed();
return m_cached_name_to_element_mappings->contains(name);
}
// https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-names
Vector<FlyString> HTMLCollection::supported_property_names() const
{
// 1. Let result be an empty list.
Vector<FlyString> result;
// 2. For each element represented by the collection, in tree order:
update_name_to_element_mappings_if_needed();
for (auto const& it : *m_cached_name_to_element_mappings) {
result.append(it.key);
}
// 3. Return result.
return result;
}
Optional<JS::Value> HTMLCollection::item_value(size_t index) const
{
auto* element = item(index);
if (!element)
return {};
return element;
}
JS::Value HTMLCollection::named_item_value(FlyString const& name) const
{
auto* element = named_item(name);
if (!element)
return JS::js_undefined();
return element;
}
}