[php-src] PHP-8.4: Fix DOMNameSpaceNode clone UAF after xinclude

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

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

Fix DOMNameSpaceNode clone UAF after xinclude

Clone built the fake namespace decl from original_node->parent, which
xinclude has already freed; parent_intern is the durable handle.

Closes GH-23248

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 ecb4d105e917..338409dd70c5 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 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--
+<?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.