Commit fcdc4fbf858 for php.net

commit fcdc4fbf858c91ddfaa5d4203ea279737a102a19
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Thu Aug 13 10:35:46 2026 -0400

    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.

    Closes GH-23253

diff --git a/NEWS b/NEWS
index 1bac1989e7e..baea08f757f 100644
--- a/NEWS
+++ b/NEWS
@@ -9,6 +9,8 @@ PHP                                                                        NEWS
 - DOM:
   . Fixed a use-after-free when cloning a DOMNameSpaceNode after
     DOMDocument::xinclude(). (iliaal)
+  . 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 00000000000..4e4b2b2c282
--- /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--
+<?php
+$doc1 = new DOMDocument();
+$doc1->loadXML('<root><a>1</a></root>');
+$doc2 = new DOMDocument();
+$doc2->loadXML('<root><b>2</b></root>');
+
+$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 199dc96af40..4688ed7743e 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();