From 58c0813f9059b2e8e5940f5ccb4fd2a511e954a8 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Tue, 18 Aug 2026 05:54:53 +0100 Subject: [PATCH 1/3] ext/dom: php_dom_free_templated_content() use-after-free. Fix #23334 The fragment was freed with xmlFreeNode(), assuming no userland reference into template contents could exist. getElementById() reaches those nodes, so freeing the template element left dangling proxies. The children now go through php_libxml_node_free_list(), and the fragment is freed only when it has no proxy; otherwise its parent link is cleared so it is freed with the last reference. --- ext/dom/private_data.c | 12 +++++--- .../modern/html/interactions/gh23334.phpt | 29 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 ext/dom/tests/modern/html/interactions/gh23334.phpt diff --git a/ext/dom/private_data.c b/ext/dom/private_data.c index 66b3f4f87f29..d908ad329251 100644 --- a/ext/dom/private_data.c +++ b/ext/dom/private_data.c @@ -72,9 +72,7 @@ void php_dom_private_data_destroy(php_dom_private_data *data) static void php_dom_free_templated_content(php_dom_private_data *private_data, xmlNodePtr base) { - /* Note: it's not possible to obtain a userland reference to these yet, so we can just free them without worrying - * about their proxies. - * Note 2: it's possible to have nested template content. */ + /* Note: it's possible to have nested template content. */ if (zend_hash_num_elements(private_data->template_fragments) > 0) { /* There's more templated content, try to free it. */ @@ -88,7 +86,13 @@ static void php_dom_free_templated_content(php_dom_private_data *private_data, x } } - xmlFreeNode(base); + php_libxml_node_free_list(base->children); + + if (!base->_private) { + xmlFreeNode(base); + } else { + base->parent = NULL; + } } void php_dom_add_templated_content(php_dom_private_data *private_data, const xmlNode *template_node, xmlNodePtr fragment) diff --git a/ext/dom/tests/modern/html/interactions/gh23334.phpt b/ext/dom/tests/modern/html/interactions/gh23334.phpt new file mode 100644 index 000000000000..82a86b6c1bf8 --- /dev/null +++ b/ext/dom/tests/modern/html/interactions/gh23334.phpt @@ -0,0 +1,29 @@ +--TEST-- +GH-23334 (UAF when template contents are freed while userland references remain) +--EXTENSIONS-- +dom +--FILE-- +
'; + +// A descendant of the template contents outlives the template element. +$doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR); +$inner = $doc->getElementById('inner'); +$tpl = $doc->getElementById('tpl'); +$tpl->remove(); +unset($tpl); +var_dump($inner->textContent); +var_dump($inner->parentNode); + +// The template contents fragment itself outlives the template element. +$doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR); +$frag = $doc->getElementById('inner')->parentNode; +var_dump($frag::class); +$doc->getElementById('host')->innerHTML = ''; +var_dump($frag->childNodes->length); +?> +--EXPECT-- +string(5) "HELLO" +NULL +string(20) "Dom\DocumentFragment" +int(0) From 47dca30b61bb48a49f0090ddd6534f856123daee Mon Sep 17 00:00:00 2001 From: David Carlier Date: Tue, 18 Aug 2026 07:10:24 +0100 Subject: [PATCH 2/3] Template contents leaking into the host element's tree. Upward walks followed the fragment's parent link to its host, placing template contents in the host document instead of rooting them at the fragment. They now stop at a fragment via php_dom_parent_node(), except the cycle check which the spec resolves against host-including ancestors. --- ext/dom/node.c | 22 +++---- ext/dom/parentnode/css_selectors.c | 2 +- ext/dom/php_dom.h | 5 ++ .../html/interactions/gh23334_tree.phpt | 57 +++++++++++++++++++ 4 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 ext/dom/tests/modern/html/interactions/gh23334_tree.phpt diff --git a/ext/dom/node.c b/ext/dom/node.c index 49e400ef5302..336a5290cf63 100644 --- a/ext/dom/node.c +++ b/ext/dom/node.c @@ -66,7 +66,7 @@ bool php_dom_is_node_connected(const xmlNode *node) if (node->type == XML_DOCUMENT_NODE || node->type == XML_HTML_DOCUMENT_NODE) { return true; } - node = node->parent; + node = php_dom_parent_node(node); } while (node != NULL); return false; } @@ -244,7 +244,7 @@ static zend_result dom_node_parent_get(dom_object *obj, zval *retval, bool only_ { DOM_PROP_NODE(xmlNodePtr, nodep, obj); - xmlNodePtr nodeparent = nodep->parent; + xmlNodePtr nodeparent = php_dom_parent_node(nodep); if (!nodeparent || (only_element && nodeparent->type != XML_ELEMENT_NODE)) { ZVAL_NULL(retval); return SUCCESS; @@ -2415,7 +2415,7 @@ static bool dom_node_contains(xmlNodePtr thisp, xmlNodePtr otherp) if (otherp == thisp) { return true; } - otherp = otherp->parent; + otherp = php_dom_parent_node(otherp); } while (otherp); return false; @@ -2473,7 +2473,7 @@ PHP_METHOD(Dom_Node, contains) PHP_METHOD(DOMNode, getRootNode) { zval *id; - xmlNodePtr thisp; + xmlNodePtr thisp, tmp; dom_object *intern; /* Unused now because we don't support the shadow DOM nodes. Options only influence shadow DOM nodes. */ zval *options; @@ -2485,8 +2485,8 @@ PHP_METHOD(DOMNode, getRootNode) DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, intern); - while (thisp->parent) { - thisp = thisp->parent; + while ((tmp = php_dom_parent_node(thisp))) { + thisp = tmp; } DOM_RET_OBJ(thisp, intern); @@ -2562,9 +2562,9 @@ static void dom_node_compare_document_position(INTERNAL_FUNCTION_PARAMETERS, zen } bool node2_is_ancestor_of_node1 = false; size_t node1_depth = 0; - xmlNodePtr node1_root = node1; - while (node1_root->parent) { - node1_root = node1_root->parent; + xmlNodePtr node1_root = node1, tmp; + while ((tmp = php_dom_parent_node(node1_root))) { + node1_root = tmp; if (node1_root == node2) { node2_is_ancestor_of_node1 = true; } @@ -2573,8 +2573,8 @@ static void dom_node_compare_document_position(INTERNAL_FUNCTION_PARAMETERS, zen bool node1_is_ancestor_of_node2 = false; size_t node2_depth = 0; xmlNodePtr node2_root = node2; - while (node2_root->parent) { - node2_root = node2_root->parent; + while ((tmp = php_dom_parent_node(node2_root))) { + node2_root = tmp; if (node2_root == node1) { node1_is_ancestor_of_node2 = true; } diff --git a/ext/dom/parentnode/css_selectors.c b/ext/dom/parentnode/css_selectors.c index 43012c4ebb99..c536d787533e 100644 --- a/ext/dom/parentnode/css_selectors.c +++ b/ext/dom/parentnode/css_selectors.c @@ -200,7 +200,7 @@ static const xmlNode *dom_query_closest( ret = current; break; } - current = current->parent; + current = php_dom_parent_node(current); } } diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index d399d745084b..43ab1d4d5def 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -248,6 +248,11 @@ xmlNodePtr dom_nodelist_iter_start_first_child(xmlNodePtr nodep); __ptr = (__prtype)((php_libxml_node_ptr *)__intern->ptr)->node; \ } +static zend_always_inline xmlNodePtr php_dom_parent_node(const xmlNode *nodep) +{ + return nodep->type == XML_DOCUMENT_FRAG_NODE ? NULL : nodep->parent; +} + static zend_always_inline bool php_dom_is_cache_tag_stale_from_doc_ptr(const php_libxml_cache_tag *cache_tag, const php_libxml_ref_obj *doc_ptr) { ZEND_ASSERT(doc_ptr != NULL); diff --git a/ext/dom/tests/modern/html/interactions/gh23334_tree.phpt b/ext/dom/tests/modern/html/interactions/gh23334_tree.phpt new file mode 100644 index 000000000000..6e6b666d7fdf --- /dev/null +++ b/ext/dom/tests/modern/html/interactions/gh23334_tree.phpt @@ -0,0 +1,57 @@ +--TEST-- +GH-23334 (template contents must not be part of the host element's tree) +--EXTENSIONS-- +dom +--FILE-- +
', + LIBXML_NOERROR +); +$tpl = $doc->getElementById('tpl'); +$inner = $doc->getElementById('inner'); +$frag = $inner->parentNode; + +var_dump($frag::class); +var_dump($frag->parentNode); +var_dump($frag->parentElement); +var_dump($frag->getRootNode() === $frag); +var_dump($inner->getRootNode() === $frag); +var_dump($frag->isConnected); +var_dump($inner->isConnected); + +var_dump($frag->contains($inner)); +var_dump($tpl->contains($inner)); +var_dump($doc->body->contains($inner)); + +var_dump($inner->closest('b')?->nodeName); +var_dump($inner->closest('#host')); +var_dump($inner->closest('body')); + +$pos = $tpl->compareDocumentPosition($inner); +var_dump((bool) ($pos & 1)); +var_dump((bool) ($pos & 0x10)); + +try { + $frag->appendChild($tpl); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +?> +--EXPECT-- +string(20) "Dom\DocumentFragment" +NULL +NULL +bool(true) +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +bool(false) +string(1) "B" +NULL +NULL +bool(true) +bool(false) +DOMException: Hierarchy Request Error From 121f1774b155e15d202eb2217b4b183c52f4342b Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 18 Oct 2026 02:43:36 +0100 Subject: [PATCH 3/3] feedback --- ext/dom/html5_parser.c | 19 +++++-- ext/dom/html5_parser.h | 1 + ext/dom/inner_html_mixin.c | 9 +-- ext/dom/node.c | 22 +++---- ext/dom/parentnode/css_selectors.c | 2 +- ext/dom/php_dom.h | 5 -- ext/dom/private_data.c | 12 ++-- .../modern/html/interactions/gh23334.phpt | 25 ++++---- .../html/interactions/gh23334_tree.phpt | 57 ------------------- 9 files changed, 48 insertions(+), 104 deletions(-) delete mode 100644 ext/dom/tests/modern/html/interactions/gh23334_tree.phpt diff --git a/ext/dom/html5_parser.c b/ext/dom/html5_parser.c index 24f89cb0fafb..d946979b705d 100644 --- a/ext/dom/html5_parser.c +++ b/ext/dom/html5_parser.c @@ -39,6 +39,7 @@ typedef struct work_list_item { uintptr_t current_active_namespace; xmlNodePtr lxml_parent; xmlNsPtr lxml_ns; + bool in_template; } work_list_item; static void lexbor_libxml2_bridge_work_list_item_push( @@ -46,7 +47,8 @@ static void lexbor_libxml2_bridge_work_list_item_push( lxb_dom_node_t *node, uintptr_t current_active_namespace, xmlNodePtr lxml_parent, - xmlNsPtr lxml_ns + xmlNsPtr lxml_ns, + bool in_template ) { work_list_item *item = (work_list_item *) lexbor_array_obj_push_wo_cls(array); @@ -54,6 +56,7 @@ static void lexbor_libxml2_bridge_work_list_item_push( item->current_active_namespace = current_active_namespace; item->lxml_parent = lxml_parent; item->lxml_ns = lxml_ns; + item->in_template = in_template; } static unsigned short sanitize_line_nr(size_t line) @@ -109,6 +112,7 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert( xmlNodePtr root, bool compact_text_nodes, bool create_default_ns, + bool in_template_contents, php_dom_private_data *private_data ) { @@ -124,13 +128,14 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert( lexbor_array_obj_init(&work_list, WORK_LIST_INIT_SIZE, sizeof(work_list_item)); for (lxb_dom_node_t *node = start_node; node != NULL; node = node->prev) { - lexbor_libxml2_bridge_work_list_item_push(&work_list, node, LXB_NS__UNDEF, root, NULL); + lexbor_libxml2_bridge_work_list_item_push(&work_list, node, LXB_NS__UNDEF, root, NULL, in_template_contents); } work_list_item *current_stack_item; while ((current_stack_item = lexbor_array_obj_pop(&work_list)) != NULL) { lxb_dom_node_t *node = current_stack_item->node; xmlNodePtr lxml_parent = current_stack_item->lxml_parent; + bool in_template = current_stack_item->in_template; /* CDATA section and processing instructions don't occur in parsed HTML documents. * The historical types are not emitted by the parser either. */ @@ -173,7 +178,9 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert( * Other types of nodes contain their children directly. */ xmlNodePtr lxml_child_parent = lxml_element; lxb_dom_node_t *child_node = element->node.last_child; + bool child_in_template = in_template; if (lxb_html_tree_node_is(&element->node, LXB_TAG_TEMPLATE)) { + child_in_template = true; if (create_default_ns) { lxml_child_parent = xmlNewDocFragment(lxml_doc); if (UNEXPECTED(lxml_child_parent == NULL)) { @@ -198,7 +205,8 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert( child_node, entering_namespace, lxml_child_parent, - current_lxml_ns + current_lxml_ns, + child_in_template ); } @@ -275,7 +283,7 @@ static lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert( /* xmlIsID does some other stuff too that is irrelevant here. */ if (local_name_length == 2 && local_name[0] == 'i' && local_name[1] == 'd' && lxml_attr->ns == NULL) { - if (xmlAddID(NULL, lxml_doc, value, lxml_attr) == 0) { + if (in_template || xmlAddID(NULL, lxml_doc, value, lxml_attr) == 0) { /* If the ID already exists, the ID attribute still needs to be marked as an ID. */ lxml_attr->atype = XML_ATTRIBUTE_ID; } @@ -369,6 +377,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_document( (xmlNodePtr) lxml_doc, compact_text_nodes, create_default_ns, + false, private_data ); if (status != LEXBOR_LIBXML2_BRIDGE_STATUS_OK) { @@ -385,6 +394,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_fragment( xmlNodePtr *fragment_out, bool compact_text_nodes, bool create_default_ns, + bool in_template_contents, php_dom_private_data *private_data ) { @@ -398,6 +408,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_fragment( fragment, compact_text_nodes, create_default_ns, + in_template_contents, private_data ); if (status != LEXBOR_LIBXML2_BRIDGE_STATUS_OK) { diff --git a/ext/dom/html5_parser.h b/ext/dom/html5_parser.h index 76e60ea8c676..98f47eedf744 100644 --- a/ext/dom/html5_parser.h +++ b/ext/dom/html5_parser.h @@ -79,6 +79,7 @@ lexbor_libxml2_bridge_status lexbor_libxml2_bridge_convert_fragment( xmlNodePtr *fragment_out, bool compact_text_nodes, bool create_default_ns, + bool in_template_contents, php_dom_private_data *private_data ); void lexbor_libxml2_bridge_report_errors( diff --git a/ext/dom/inner_html_mixin.c b/ext/dom/inner_html_mixin.c index e81ca5a7b238..ef7f3051dc4b 100644 --- a/ext/dom/inner_html_mixin.c +++ b/ext/dom/inner_html_mixin.c @@ -181,7 +181,7 @@ static lxb_dom_document_cmode_t dom_translate_quirks_mode(php_libxml_quirks_mode } /* https://html.spec.whatwg.org/#html-fragment-parsing-algorithm */ -static xmlNodePtr dom_html_fragment_parsing_algorithm(dom_object *obj, xmlNodePtr context_node, const zend_string *input, php_libxml_quirks_mode quirks_mode) +static xmlNodePtr dom_html_fragment_parsing_algorithm(dom_object *obj, xmlNodePtr context_node, const zend_string *input, php_libxml_quirks_mode quirks_mode, bool is_template) { /* The whole algorithm is implemented in Lexbor, we just have to be the adapter between the * data structures used in PHP and what Lexbor expects. */ @@ -209,7 +209,7 @@ static xmlNodePtr dom_html_fragment_parsing_algorithm(dom_object *obj, xmlNodePt xmlNodePtr fragment = NULL; if (node != NULL) { /* node->last_child could be NULL, but that is allowed. */ - lexbor_libxml2_bridge_status status = lexbor_libxml2_bridge_convert_fragment(node->last_child, context_node->doc, &fragment, true, true, php_dom_get_private_data(obj)); + lexbor_libxml2_bridge_status status = lexbor_libxml2_bridge_convert_fragment(node->last_child, context_node->doc, &fragment, true, true, is_template, php_dom_get_private_data(obj)); if (UNEXPECTED(status != LEXBOR_LIBXML2_BRIDGE_STATUS_OK)) { php_dom_throw_error(INVALID_STATE_ERR, true); } @@ -342,18 +342,19 @@ zend_result dom_element_inner_html_write(dom_object *obj, zval *newval) { DOM_PROP_NODE(xmlNodePtr, context_node, obj); + bool is_template = php_dom_ns_is_fast(context_node, php_dom_ns_is_html_magic_token) && xmlStrEqual(context_node->name, BAD_CAST "template"); xmlNodePtr fragment; if (context_node->doc->type == XML_DOCUMENT_NODE) { fragment = dom_xml_fragment_parsing_algorithm(obj, context_node, Z_STR_P(newval)); } else { - fragment = dom_html_fragment_parsing_algorithm(obj, context_node, Z_STR_P(newval), obj->document->quirks_mode); + fragment = dom_html_fragment_parsing_algorithm(obj, context_node, Z_STR_P(newval), obj->document->quirks_mode, is_template); } if (fragment == NULL) { return FAILURE; } - if (php_dom_ns_is_fast(context_node, php_dom_ns_is_html_magic_token) && xmlStrEqual(context_node->name, BAD_CAST "template")) { + if (is_template) { context_node = php_dom_ensure_templated_content(php_dom_get_private_data(obj), context_node); if (context_node == NULL) { xmlFreeNode(fragment); diff --git a/ext/dom/node.c b/ext/dom/node.c index 336a5290cf63..49e400ef5302 100644 --- a/ext/dom/node.c +++ b/ext/dom/node.c @@ -66,7 +66,7 @@ bool php_dom_is_node_connected(const xmlNode *node) if (node->type == XML_DOCUMENT_NODE || node->type == XML_HTML_DOCUMENT_NODE) { return true; } - node = php_dom_parent_node(node); + node = node->parent; } while (node != NULL); return false; } @@ -244,7 +244,7 @@ static zend_result dom_node_parent_get(dom_object *obj, zval *retval, bool only_ { DOM_PROP_NODE(xmlNodePtr, nodep, obj); - xmlNodePtr nodeparent = php_dom_parent_node(nodep); + xmlNodePtr nodeparent = nodep->parent; if (!nodeparent || (only_element && nodeparent->type != XML_ELEMENT_NODE)) { ZVAL_NULL(retval); return SUCCESS; @@ -2415,7 +2415,7 @@ static bool dom_node_contains(xmlNodePtr thisp, xmlNodePtr otherp) if (otherp == thisp) { return true; } - otherp = php_dom_parent_node(otherp); + otherp = otherp->parent; } while (otherp); return false; @@ -2473,7 +2473,7 @@ PHP_METHOD(Dom_Node, contains) PHP_METHOD(DOMNode, getRootNode) { zval *id; - xmlNodePtr thisp, tmp; + xmlNodePtr thisp; dom_object *intern; /* Unused now because we don't support the shadow DOM nodes. Options only influence shadow DOM nodes. */ zval *options; @@ -2485,8 +2485,8 @@ PHP_METHOD(DOMNode, getRootNode) DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, intern); - while ((tmp = php_dom_parent_node(thisp))) { - thisp = tmp; + while (thisp->parent) { + thisp = thisp->parent; } DOM_RET_OBJ(thisp, intern); @@ -2562,9 +2562,9 @@ static void dom_node_compare_document_position(INTERNAL_FUNCTION_PARAMETERS, zen } bool node2_is_ancestor_of_node1 = false; size_t node1_depth = 0; - xmlNodePtr node1_root = node1, tmp; - while ((tmp = php_dom_parent_node(node1_root))) { - node1_root = tmp; + xmlNodePtr node1_root = node1; + while (node1_root->parent) { + node1_root = node1_root->parent; if (node1_root == node2) { node2_is_ancestor_of_node1 = true; } @@ -2573,8 +2573,8 @@ static void dom_node_compare_document_position(INTERNAL_FUNCTION_PARAMETERS, zen bool node1_is_ancestor_of_node2 = false; size_t node2_depth = 0; xmlNodePtr node2_root = node2; - while ((tmp = php_dom_parent_node(node2_root))) { - node2_root = tmp; + while (node2_root->parent) { + node2_root = node2_root->parent; if (node2_root == node1) { node1_is_ancestor_of_node2 = true; } diff --git a/ext/dom/parentnode/css_selectors.c b/ext/dom/parentnode/css_selectors.c index c536d787533e..43012c4ebb99 100644 --- a/ext/dom/parentnode/css_selectors.c +++ b/ext/dom/parentnode/css_selectors.c @@ -200,7 +200,7 @@ static const xmlNode *dom_query_closest( ret = current; break; } - current = php_dom_parent_node(current); + current = current->parent; } } diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index 43ab1d4d5def..d399d745084b 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -248,11 +248,6 @@ xmlNodePtr dom_nodelist_iter_start_first_child(xmlNodePtr nodep); __ptr = (__prtype)((php_libxml_node_ptr *)__intern->ptr)->node; \ } -static zend_always_inline xmlNodePtr php_dom_parent_node(const xmlNode *nodep) -{ - return nodep->type == XML_DOCUMENT_FRAG_NODE ? NULL : nodep->parent; -} - static zend_always_inline bool php_dom_is_cache_tag_stale_from_doc_ptr(const php_libxml_cache_tag *cache_tag, const php_libxml_ref_obj *doc_ptr) { ZEND_ASSERT(doc_ptr != NULL); diff --git a/ext/dom/private_data.c b/ext/dom/private_data.c index d908ad329251..66b3f4f87f29 100644 --- a/ext/dom/private_data.c +++ b/ext/dom/private_data.c @@ -72,7 +72,9 @@ void php_dom_private_data_destroy(php_dom_private_data *data) static void php_dom_free_templated_content(php_dom_private_data *private_data, xmlNodePtr base) { - /* Note: it's possible to have nested template content. */ + /* Note: it's not possible to obtain a userland reference to these yet, so we can just free them without worrying + * about their proxies. + * Note 2: it's possible to have nested template content. */ if (zend_hash_num_elements(private_data->template_fragments) > 0) { /* There's more templated content, try to free it. */ @@ -86,13 +88,7 @@ static void php_dom_free_templated_content(php_dom_private_data *private_data, x } } - php_libxml_node_free_list(base->children); - - if (!base->_private) { - xmlFreeNode(base); - } else { - base->parent = NULL; - } + xmlFreeNode(base); } void php_dom_add_templated_content(php_dom_private_data *private_data, const xmlNode *template_node, xmlNodePtr fragment) diff --git a/ext/dom/tests/modern/html/interactions/gh23334.phpt b/ext/dom/tests/modern/html/interactions/gh23334.phpt index 82a86b6c1bf8..e6425e640fb3 100644 --- a/ext/dom/tests/modern/html/interactions/gh23334.phpt +++ b/ext/dom/tests/modern/html/interactions/gh23334.phpt @@ -1,29 +1,26 @@ --TEST-- -GH-23334 (UAF when template contents are freed while userland references remain) +GH-23334 (Template contents must not be reachable through the ID cache) --EXTENSIONS-- dom --FILE--
'; -// A descendant of the template contents outlives the template element. $doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR); -$inner = $doc->getElementById('inner'); -$tpl = $doc->getElementById('tpl'); -$tpl->remove(); -unset($tpl); -var_dump($inner->textContent); -var_dump($inner->parentNode); +var_dump($doc->getElementById('inner')); +var_dump((new Dom\XPath($doc))->query("id('inner')")->length); +var_dump($doc->getElementById('tpl')->innerHTML); -// The template contents fragment itself outlives the template element. $doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR); -$frag = $doc->getElementById('inner')->parentNode; -var_dump($frag::class); +$tpl = $doc->getElementById('tpl'); +$tpl->innerHTML = '

WORLD

'; +var_dump((new Dom\XPath($doc))->query("id('set')")->length); +var_dump($tpl->innerHTML); $doc->getElementById('host')->innerHTML = ''; -var_dump($frag->childNodes->length); ?> --EXPECT-- -string(5) "HELLO" NULL -string(20) "Dom\DocumentFragment" int(0) +string(27) "
HELLO
" +int(0) +string(21) "

WORLD

" diff --git a/ext/dom/tests/modern/html/interactions/gh23334_tree.phpt b/ext/dom/tests/modern/html/interactions/gh23334_tree.phpt deleted file mode 100644 index 6e6b666d7fdf..000000000000 --- a/ext/dom/tests/modern/html/interactions/gh23334_tree.phpt +++ /dev/null @@ -1,57 +0,0 @@ ---TEST-- -GH-23334 (template contents must not be part of the host element's tree) ---EXTENSIONS-- -dom ---FILE-- -
', - LIBXML_NOERROR -); -$tpl = $doc->getElementById('tpl'); -$inner = $doc->getElementById('inner'); -$frag = $inner->parentNode; - -var_dump($frag::class); -var_dump($frag->parentNode); -var_dump($frag->parentElement); -var_dump($frag->getRootNode() === $frag); -var_dump($inner->getRootNode() === $frag); -var_dump($frag->isConnected); -var_dump($inner->isConnected); - -var_dump($frag->contains($inner)); -var_dump($tpl->contains($inner)); -var_dump($doc->body->contains($inner)); - -var_dump($inner->closest('b')?->nodeName); -var_dump($inner->closest('#host')); -var_dump($inner->closest('body')); - -$pos = $tpl->compareDocumentPosition($inner); -var_dump((bool) ($pos & 1)); -var_dump((bool) ($pos & 0x10)); - -try { - $frag->appendChild($tpl); -} catch (\Throwable $e) { - echo $e::class, ': ', $e->getMessage(), PHP_EOL; -} -?> ---EXPECT-- -string(20) "Dom\DocumentFragment" -NULL -NULL -bool(true) -bool(true) -bool(false) -bool(false) -bool(true) -bool(false) -bool(false) -string(1) "B" -NULL -NULL -bool(true) -bool(false) -DOMException: Hierarchy Request Error