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();