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)