[php-src] master: Merge branch 'PHP-8.4' into 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:26-04:00

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

Merge branch 'PHP-8.4' into PHP-8.5

* PHP-8.4:
  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 b62200e4c06a..1bac1989e7ed 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)
+
 - Opcache:
   . Fixed opcache.protect_memory race under ZTS. (realFlowControl)
 
diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c
index 6716315b82b9..1eccb5140278 100644
--- a/ext/dom/php_dom.c
+++ b/ext/dom/php_dom.c
@@ -147,7 +147,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);
@@ -722,7 +722,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);
 	}
 
@@ -2255,15 +2256,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;
@@ -2274,7 +2276,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.