[php-src] master: Merge branch 'PHP-8.5'

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-08-17T06:44:35-04:00

Commit: https://github.com/php/php-src/commit/affde50531cf6d6ef97df1866d363daa60302e26
Raw diff: https://github.com/php/php-src/commit/affde50531cf6d6ef97df1866d363daa60302e26.diff

Merge branch 'PHP-8.5'

* PHP-8.5:
  Fix DOMNameSpaceNode clone UAF after xinclude

Changed paths:
  A  ext/dom/tests/dom_namespacenode_clone_xinclude.phpt
  M  NEWS
  M  ext/dom/php_dom.c


Diff:

diff --git a/NEWS b/NEWS
index a75d304276f3..8e7950150646 100644
--- a/NEWS
+++ b/NEWS
@@ -17,6 +17,8 @@ PHP                                                                        NEWS
 - DOM:
   . Fixed bug GH-22624 (use-after-free via DOMNameSpaceNode after
     DOMDocument::xinclude()). (David Carlier)
+  . Fixed a use-after-free when cloning a DOMNameSpaceNode after
+    DOMDocument::xinclude(). (iliaal)
 
 - PDO_PGSQL:
   . Fixed several lazy fetch (PDO::ATTR_PREFETCH => 0) defects: an infinite
diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c
index 467271943275..158534530c3e 100644
--- a/ext/dom/php_dom.c
+++ b/ext/dom/php_dom.c
@@ -145,7 +145,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);
@@ -725,7 +725,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);
 	}
 
@@ -2262,15 +2263,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;
@@ -2281,7 +2283,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--
+<?php
+$included = __DIR__ . '/dom_namespacenode_clone_xinclude_included.xml';
+file_put_contents($included, '<?xml version="1.0"?><included/>');
+$href = 'file:///' . ltrim(str_replace('\\', '/', $included), '/');
+
+$doc = new DOMDocument();
+$doc->loadXML('<?xml version="1.0"?>
+<root xmlns:xi="http://www.w3.org/2001/XInclude">
+  <xi:include href="' . $href . '" xmlns:local="urn:test"/>
+</root>');
+
+$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--
+<?php
+@unlink(__DIR__ . '/dom_namespacenode_clone_xinclude_included.xml');
+?>
+--EXPECT--
+live clone: xmlns:local
+live parent: xi:include
+after xinclude: xmlns:local
+NULL
+NULL
+bool(false)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.