From 718df2281c8536caae5fd4c684c2f506fce1f4ef Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 13 Aug 2026 10:35:46 -0400 Subject: [PATCH] Fix DOMXPath crash wrapping a foreign node after a nodeset callback dom_xpath_intern_for_doc walked xpath_callbacks.node_list with Z_DOMOBJ_P on every entry. That list also stores arrays (nodeset-mode callback arguments). Same-document results take the early return; a later php:function that returns a node from another document walks the leftover array first. Skip non-objects and recurse into arrays so the matching intern is found instead of type-confusing the array. --- NEWS | 4 ++ .../tests/xpath_php_function_foreign_doc.phpt | 40 +++++++++++++++++++ ext/dom/xpath.c | 23 ++++++++++- 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 ext/dom/tests/xpath_php_function_foreign_doc.phpt diff --git a/NEWS b/NEWS index b62200e4c06a..94db73510595 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 crash in DOMXPath when a php:function callback receives a nodeset + and a later callback returns a node from another document. (iliaal) + - Opcache: . Fixed opcache.protect_memory race under ZTS. (realFlowControl) diff --git a/ext/dom/tests/xpath_php_function_foreign_doc.phpt b/ext/dom/tests/xpath_php_function_foreign_doc.phpt new file mode 100644 index 000000000000..4e4b2b2c2822 --- /dev/null +++ b/ext/dom/tests/xpath_php_function_foreign_doc.phpt @@ -0,0 +1,40 @@ +--TEST-- +DOMXPath: php:function nodeset args plus a foreign-document return must not treat arrays as DOM objects +--EXTENSIONS-- +dom +--FILE-- +loadXML('1'); +$doc2 = new DOMDocument(); +$doc2->loadXML('2'); + +$xp = new DOMXPath($doc1); +$xp->registerNamespace('php', 'http://php.net/xpath'); +$xp->registerPhpFunctions(); + +function uses_nodeset($nodes) { + return true; +} + +function foreign() { + global $doc2; + return $doc2->documentElement; +} + +$xp->query('//a[php:function("uses_nodeset", //a)]'); +$res = $xp->query('php:function("foreign")'); +echo "count: "; +var_dump($res->length); +$n = $res->item(0); +echo "name: "; +var_dump($n->nodeName); +echo "owner is doc2: "; +var_dump($n->ownerDocument === $doc2); +echo "done\n"; +?> +--EXPECT-- +count: int(1) +name: string(4) "root" +owner is doc2: bool(true) +done diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c index 199dc96af40e..4688ed7743eb 100644 --- a/ext/dom/xpath.c +++ b/ext/dom/xpath.c @@ -35,6 +35,25 @@ #ifdef LIBXML_XPATH_ENABLED +static dom_object *dom_xpath_intern_from_entry(zval *entry, xmlDocPtr doc) +{ + if (Z_TYPE_P(entry) == IS_OBJECT) { + dom_object *obj = Z_DOMOBJ_P(entry); + if (obj->document && obj->document->ptr == doc) { + return obj; + } + } else if (Z_TYPE_P(entry) == IS_ARRAY) { + zval *inner; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(entry), inner) { + dom_object *obj = dom_xpath_intern_from_entry(inner, doc); + if (obj) { + return obj; + } + } ZEND_HASH_FOREACH_END(); + } + return NULL; +} + static dom_object *dom_xpath_intern_for_doc(dom_xpath_object *xpath_obj, xmlDocPtr doc) { if (xpath_obj->dom.document && xpath_obj->dom.document->ptr == doc) { @@ -44,8 +63,8 @@ static dom_object *dom_xpath_intern_for_doc(dom_xpath_object *xpath_obj, xmlDocP if (node_list) { zval *entry; ZEND_HASH_PACKED_FOREACH_VAL(node_list, entry) { - dom_object *obj = Z_DOMOBJ_P(entry); - if (obj->document && obj->document->ptr == doc) { + dom_object *obj = dom_xpath_intern_from_entry(entry, doc); + if (obj) { return obj; } } ZEND_HASH_FOREACH_END();