diff --git a/NEWS b/NEWS index 0d9e42792078..763a370d74ee 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ PHP NEWS . Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or next() call on the inner generator). (iliaal) +- DOM: + . Fixed a use-after-free when cloning a DOMNameSpaceNode after + DOMDocument::xinclude(). (iliaal) + 27 Aug 2026, PHP 8.4.25 - Core: diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 00f63d475032..b19c3327419d 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -146,7 +146,7 @@ static HashTable dom_xpath_prop_handlers; static zend_object *dom_objects_namespace_node_new(zend_class_entry *class_type); static void dom_object_namespace_node_free_storage(zend_object *object); -static xmlNodePtr php_dom_create_fake_namespace_decl_node_ptr(xmlNodePtr nodep, xmlNsPtr original); +static xmlNodePtr php_dom_create_fake_namespace_decl_node_ptr(xmlNodePtr nodep, xmlNsPtr original, xmlDocPtr fallback_doc); typedef zend_result (*dom_read_t)(dom_object *obj, zval *retval); typedef zend_result (*dom_write_t)(dom_object *obj, zval *newval); @@ -705,7 +705,8 @@ static zend_object *dom_object_namespace_node_clone_obj(zend_object *zobject) xmlNodePtr original_node = dom_object_get_node(&intern->dom); if (original_node != NULL) { ZEND_ASSERT(original_node->type == XML_NAMESPACE_DECL); - xmlNodePtr cloned_node = php_dom_create_fake_namespace_decl_node_ptr(original_node->parent, original_node->ns); + xmlNodePtr parent = intern->parent_intern ? dom_object_get_node(intern->parent_intern) : NULL; + xmlNodePtr cloned_node = php_dom_create_fake_namespace_decl_node_ptr(parent, original_node->ns, original_node->doc); dom_update_refcount_after_clone(&intern->dom, original_node, &clone_intern->dom, cloned_node); } @@ -2318,15 +2319,16 @@ xmlNsPtr dom_get_nsdecl(xmlNode *node, xmlChar *localName) { } /* }}} end dom_get_nsdecl */ -static xmlNodePtr php_dom_create_fake_namespace_decl_node_ptr(xmlNodePtr nodep, xmlNsPtr original) +static xmlNodePtr php_dom_create_fake_namespace_decl_node_ptr(xmlNodePtr nodep, xmlNsPtr original, xmlDocPtr fallback_doc) { xmlNodePtr attrp; + xmlDocPtr doc = nodep ? nodep->doc : fallback_doc; xmlNsPtr curns = xmlNewNs(NULL, original->href, NULL); if (original->prefix) { curns->prefix = xmlStrdup(original->prefix); - attrp = xmlNewDocNode(nodep->doc, NULL, BAD_CAST original->prefix, original->href); + attrp = xmlNewDocNode(doc, NULL, BAD_CAST original->prefix, original->href); } else { - attrp = xmlNewDocNode(nodep->doc, NULL, BAD_CAST "xmlns", original->href); + attrp = xmlNewDocNode(doc, NULL, BAD_CAST "xmlns", original->href); } attrp->type = XML_NAMESPACE_DECL; attrp->parent = nodep; @@ -2337,7 +2339,7 @@ static xmlNodePtr php_dom_create_fake_namespace_decl_node_ptr(xmlNodePtr nodep, /* Note: Assumes the additional lifetime was already added in the caller. */ xmlNodePtr php_dom_create_fake_namespace_decl(xmlNodePtr nodep, xmlNsPtr original, zval *return_value, dom_object *parent_intern) { - xmlNodePtr attrp = php_dom_create_fake_namespace_decl_node_ptr(nodep, original); + xmlNodePtr attrp = php_dom_create_fake_namespace_decl_node_ptr(nodep, original, NULL); php_dom_create_object(attrp, return_value, parent_intern); /* This object must exist, because we just created an object for it via php_dom_create_object(). */ php_dom_namespace_node_obj_from_obj(Z_OBJ_P(return_value))->parent_intern = parent_intern; diff --git a/ext/dom/tests/dom_namespacenode_clone_xinclude.phpt b/ext/dom/tests/dom_namespacenode_clone_xinclude.phpt new file mode 100644 index 000000000000..33efc0a73b44 --- /dev/null +++ b/ext/dom/tests/dom_namespacenode_clone_xinclude.phpt @@ -0,0 +1,44 @@ +--TEST-- +DOMNameSpaceNode clone after xinclude does not use a dangling parent +--EXTENSIONS-- +dom +--FILE-- +'); +$href = 'file:///' . ltrim(str_replace('\\', '/', $included), '/'); + +$doc = new DOMDocument(); +$doc->loadXML(' + + +'); + +$xpath = new DOMXPath($doc); +$xpath->registerNamespace('xi', 'http://www.w3.org/2001/XInclude'); +$xi = $xpath->query('//xi:include')->item(0); +$ns = $xpath->query('namespace::local', $xi)->item(0); + +$live = clone $ns; +echo "live clone: ", $live->nodeName, "\n"; +echo "live parent: ", $live->parentNode->nodeName, "\n"; + +$doc->xinclude(); + +$clone = clone $ns; +echo "after xinclude: ", $clone->nodeName, "\n"; +var_dump($clone->parentNode); +var_dump($clone->parentElement); +var_dump($clone->isConnected); +?> +--CLEAN-- + +--EXPECT-- +live clone: xmlns:local +live parent: xi:include +after xinclude: xmlns:local +NULL +NULL +bool(false)