Commit 33a49bb4d39 for php.net

commit 33a49bb4d390ad901ba900683a399363ce4e32fe
Author: Joe Afflerbach <git@afflerbach.info>
Date:   Mon May 18 10:23:24 2026 +0200

    ext/dom: Fix UAF in custom XPath function

    Co-authored-by: David CARLIER <devnexen@gmail.com>

    close GH-22078

diff --git a/NEWS b/NEWS
index cb68d7a8da1..d0e3104f82d 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,10 @@ PHP                                                                        NEWS
 - CLI:
   . Fixed bug GH-21901 (Stale getopt() optional value). (onthebed)

+- DOM:
+  . Fixed bug GH-22077 (UAF in custom XPath function).
+    (afflerbach/David Carlier)
+
 - Opcache:
   . Fixed tracing JIT crash when a VM interrupt is handled during an observed
     user function call. (Levi Morrison)
diff --git a/ext/dom/tests/gh22077.phpt b/ext/dom/tests/gh22077.phpt
new file mode 100644
index 00000000000..fd4e42cc8aa
--- /dev/null
+++ b/ext/dom/tests/gh22077.phpt
@@ -0,0 +1,20 @@
+--TEST--
+GH-22077 (UAF in custom XPath function)
+--FILE--
+<?php
+$document = new DOMDocument;
+$xpath = new DOMXPath($document);
+$xpath->registerNamespace("my", "my.ns");
+$xpath->registerPHPFunctionNS('my.ns', 'include', function(): DOMElement {
+    $includedDocument = new DOMDocument;
+    $includedDocument->loadXML('<root><uaf/><node/><uaf/></root>');
+    return $includedDocument->documentElement;
+});
+$nodeset = $xpath->query('my:include()/uaf');
+$node = $nodeset->item(0);
+var_dump($nodeset->length);
+var_dump($node->ownerDocument->saveXML($node));
+?>
+--EXPECT--
+int(2)
+string(6) "<uaf/>"
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index 21baa59ffed..199dc96af40 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -35,6 +35,24 @@

 #ifdef LIBXML_XPATH_ENABLED

+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) {
+		return &xpath_obj->dom;
+	}
+	HashTable *node_list = xpath_obj->xpath_callbacks.node_list;
+	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) {
+				return obj;
+			}
+		} ZEND_HASH_FOREACH_END();
+	}
+	return &xpath_obj->dom;
+}
+
 void dom_xpath_objects_free_storage(zend_object *object)
 {
 	dom_xpath_object *intern = php_xpath_obj_from_obj(object);
@@ -357,7 +375,8 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern)

 						node = php_dom_create_fake_namespace_decl(nsparent, original, &child, parent_intern);
 					} else {
-						php_dom_create_object(node, &child, &intern->dom);
+						dom_object *parent = dom_xpath_intern_for_doc(intern, node->doc);
+						php_dom_create_object(node, &child, parent);
 					}
 					add_next_index_zval(&retval, &child);
 				}