Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 8 additions & 6 deletions ext/dom/php_dom.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
44 changes: 44 additions & 0 deletions ext/dom/tests/dom_namespacenode_clone_xinclude.phpt
Original file line number Diff line number Diff line change
@@ -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)
Loading