LibWeb: Don't crash when appending to an XML document template element

When the XML parser appends child nodes to a template element, it must
actually append the template element's contents. This special behavior
caused us to return to the wrong parent element after adding child
nodes to a template element, leading to a crash.
This commit is contained in:
Tim Ledbetter 2025-07-13 23:04:34 +01:00 committed by Jelle Raaijmakers
commit 80ccb12a12
Notes: github-actions[bot] 2025-07-14 07:16:52 +00:00
4 changed files with 54 additions and 1 deletions

View file

@ -53,6 +53,7 @@ ErrorOr<Variant<ByteString, Vector<XML::MarkupDeclaration>>> resolve_xml_resourc
XMLDocumentBuilder::XMLDocumentBuilder(DOM::Document& document, XMLScriptingSupport scripting_support) XMLDocumentBuilder::XMLDocumentBuilder(DOM::Document& document, XMLScriptingSupport scripting_support)
: m_document(document) : m_document(document)
, m_template_node_stack(document.realm().heap())
, m_current_node(m_document) , m_current_node(m_document)
, m_scripting_support(scripting_support) , m_scripting_support(scripting_support)
{ {
@ -148,6 +149,7 @@ void XMLDocumentBuilder::element_start(const XML::Name& name, HashMap<XML::Name,
} }
if (m_current_node->is_html_template_element()) { if (m_current_node->is_html_template_element()) {
// When an XML parser would append a node to a template element, it must instead append it to the template element's template contents (a DocumentFragment node). // When an XML parser would append a node to a template element, it must instead append it to the template element's template contents (a DocumentFragment node).
m_template_node_stack.append(*m_current_node);
MUST(static_cast<HTML::HTMLTemplateElement&>(*m_current_node).content()->append_child(node)); MUST(static_cast<HTML::HTMLTemplateElement&>(*m_current_node).content()->append_child(node));
} else { } else {
MUST(m_current_node->append_child(node)); MUST(m_current_node->append_child(node));
@ -229,7 +231,12 @@ void XMLDocumentBuilder::element_end(const XML::Name& name)
script_element.process_the_script_element(); script_element.process_the_script_element();
}; };
m_current_node = m_current_node->parent_node(); auto* parent = m_current_node->parent_node();
if (parent->is_document_fragment()) {
auto template_parent_node = m_template_node_stack.take_last();
parent = template_parent_node.ptr();
}
m_current_node = parent;
} }
void XMLDocumentBuilder::text(StringView data) void XMLDocumentBuilder::text(StringView data)
@ -267,6 +274,7 @@ void XMLDocumentBuilder::document_end()
// NOTE: Noop. // NOTE: Noop.
// Set the insertion point to undefined. // Set the insertion point to undefined.
m_template_node_stack.clear();
m_current_node = nullptr; m_current_node = nullptr;
// Update the current document readiness to "interactive". // Update the current document readiness to "interactive".

View file

@ -41,6 +41,7 @@ private:
Optional<FlyString> namespace_for_name(XML::Name const&); Optional<FlyString> namespace_for_name(XML::Name const&);
GC::Ref<DOM::Document> m_document; GC::Ref<DOM::Document> m_document;
GC::RootVector<GC::Ref<DOM::Node>> m_template_node_stack;
GC::Ptr<DOM::Node> m_current_node; GC::Ptr<DOM::Node> m_current_node;
XMLScriptingSupport m_scripting_support { XMLScriptingSupport::Enabled }; XMLScriptingSupport m_scripting_support { XMLScriptingSupport::Enabled };
bool m_has_error { false }; bool m_has_error { false };

View file

@ -0,0 +1,8 @@
Harness status: OK
Found 3 tests
3 Pass
Pass XMLHttpRequest: template element parsing
Pass XMLHttpRequest: template element parsing 1
Pass XMLHttpRequest: template element parsing 2

View file

@ -0,0 +1,36 @@
<!doctype html>
<title>XMLHttpRequest: template element parsing</title>
<script src=../resources/testharness.js></script>
<script src=../resources/testharnessreport.js></script>
<div id=log></div>
<script>
async_test(t => {
const client = new XMLHttpRequest
client.open("GET", "data:text/xml,<template xmlns='http://www.w3.org/1999/xhtml'><test/></template>")
client.send()
client.onload = t.step_func_done(() => {
assert_equals(client.responseXML.documentElement.childElementCount, 0)
assert_equals(client.responseXML.documentElement.content.firstChild.localName, "test")
})
})
async_test(t => {
const client = new XMLHttpRequest
client.open("GET", "data:text/xml,<template><test/></template>")
client.send()
client.onload = t.step_func_done(() => {
assert_equals(client.responseXML.documentElement.childElementCount, 1)
assert_equals(client.responseXML.documentElement.firstChild.localName, "test")
})
})
async_test(t => {
const client = new XMLHttpRequest
client.open("GET", "data:text/xml,<template xmlns='http://www.w3.org/2000/svg'><test/></template>")
client.send()
client.onload = t.step_func_done(() => {
assert_equals(client.responseXML.documentElement.childElementCount, 1)
assert_equals(client.responseXML.documentElement.firstChild.localName, "test")
})
})
</script>