Commit f36660d6450 for php.net

commit f36660d64507e0d5d9fce73739031faac8139ded
Author: David Carlier <devnexen@gmail.com>
Date:   Thu Jul 2 20:10:35 2026 +0100

    ext/dom: fix use-after-free with XPath callback returning foreign-document node.

    Fix GH-22554

    A PHP XPath callback that returns a node belonging to a document created
    inside the callback (e.g. $d->documentElement of a throwaway DOMDocument)
    parks that node in the DOMXPath node_list to keep it alive. When a sibling
    callback consumes a node navigated into that foreign document, the proxy
    object was created with the DOMXPath's own dom as parent, so it took a
    reference on the wrong document and none on the foreign one. On teardown the
    foreign document could be freed while the proxy still referenced it.

    Route the proxy factory through dom_xpath_intern_for_doc() so the created
    object shares the ref_obj of the node's actual document, mirroring the
    query-result path.

    close GH-22562

diff --git a/NEWS b/NEWS
index 0459373092f..9807e07945d 100644
--- a/NEWS
+++ b/NEWS
@@ -41,6 +41,8 @@ PHP                                                                        NEWS
     returning the first entity or notation. (Weilin Du)
   . Fixed bug GH-22623 (use after free with namespace nodes from
     XSLTProcessor::registerFunctions())/ (David Carlier)
+  . Fixed bug GH-22554 (use-after-free with XPath callback returning a node
+    from a foreign document). (David Carlier)

 - Exif:
   . Fixed bug GH-11020 (exif_read_data() emits a spurious "Illegal IFD size"
diff --git a/ext/dom/tests/gh22554.phpt b/ext/dom/tests/gh22554.phpt
new file mode 100644
index 00000000000..c8db1d87390
--- /dev/null
+++ b/ext/dom/tests/gh22554.phpt
@@ -0,0 +1,38 @@
+--TEST--
+GH-22554 (Use-after-free when an XPath callback returns a node from a document created inside the callback)
+--CREDITS--
+waseem-cve
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+
+$doc = new DOMDocument;
+$doc->loadXML('<root/>');
+
+$xp = new DOMXPath($doc);
+$xp->registerNamespace('my', 'my.ns');
+
+$xp->registerPHPFunctionNS('my.ns', 'include', function () {
+    $d = new DOMDocument;
+    $d->loadXML('<r><uaf/></r>');
+
+    return $d->documentElement;
+});
+
+$xp->registerPHPFunctionNS('my.ns', 'process', function ($arg) {
+    echo "process arg: ", get_class($arg[0]), " ", $arg[0]->nodeName, "\n";
+    return 'x';
+});
+
+$result = $xp->query('my:process(my:include()/uaf)');
+var_dump($result->length);
+unset($xp);
+
+echo "Done\n";
+
+?>
+--EXPECT--
+process arg: DOMElement uaf
+int(0)
+Done
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index bd9947b4ad1..dc9e1b852fa 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -77,7 +77,8 @@ static void dom_xpath_proxy_factory(xmlNodePtr node, zval *child, dom_object *in

 	ZEND_ASSERT(node->type != XML_NAMESPACE_DECL);

-	php_dom_create_object(node, child, intern);
+	dom_xpath_object *xobj = php_xpath_obj_from_obj(&intern->std);
+	php_dom_create_object(node, child, dom_xpath_intern_for_doc(xobj, node->doc));
 }

 static dom_xpath_object *dom_xpath_ext_fetch_intern(xmlXPathParserContextPtr ctxt)