Commit 523127fd54c for php

commit 523127fd54c69e30dd10b9671ed0b59b970dd438
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Fri Sep 25 03:09:12 2026 -0400

    ext/dom: Restore the XPath context after a reentrant evaluation

    php_xpath_eval() cleared the shared xmlXPathContext's node after every
    evaluation, and with registerNodeNS (the default) its namespace list as
    well. When a php:function() callback evaluated another expression on the
    same DOMXPath or Dom\XPath, the outer evaluation resumed without its
    context node and in-scope namespaces, so later predicate steps matched
    nothing or failed with "Undefined namespace prefix". The previous node
    and namespace list are now restored instead.

    Closes GH-24125

diff --git a/NEWS b/NEWS
index 91ed81e4018..9ccefd5fddb 100644
--- a/NEWS
+++ b/NEWS
@@ -48,6 +48,9 @@ PHP                                                                        NEWS
     evaluation). (David Carlier)
   . Fixed bug GH-23897 (php:function() assertion failure after a failed
     registerPHPFunctions()). (David Carlier)
+  . Fixed DOMXPath and Dom\XPath losing the context node and in-scope
+    namespaces of an evaluation after a php:function() callback evaluated
+    another expression on the same object. (Ilia Alshanetsky)

 - FFI:
   . Fixed crashes with FFI callbacks created from __call() trampolines
diff --git a/ext/dom/tests/xpath_reentrant_context.phpt b/ext/dom/tests/xpath_reentrant_context.phpt
new file mode 100644
index 00000000000..4a307ae1dfc
--- /dev/null
+++ b/ext/dom/tests/xpath_reentrant_context.phpt
@@ -0,0 +1,54 @@
+--TEST--
+DOMXPath: Reentrant evaluation must restore the context node and namespaces
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+$doc = new DOMDocument();
+$doc->loadXML('<r xmlns:p="urn:p"><a><x>A</x><p:y/></a><a><x>A2</x></a><b><x>B</x><x>B2</x><x>B3</x></b></r>');
+$xp = new DOMXPath($doc);
+$xp->registerNamespace('php', 'http://php.net/xpath');
+$xp->registerPhpFunctions();
+$b = $doc->documentElement->lastElementChild;
+$GLOBALS['xp'] = $xp;
+$GLOBALS['b'] = $b;
+function callback($node) {
+    $GLOBALS['xp']->query('x[2]', $GLOBALS['b']);
+    return true;
+}
+
+echo "context node:\n";
+var_dump($xp->query('a[php:function("callback", .) and x]', $doc->documentElement)->length);
+var_dump($xp->evaluate('count(a[php:function("callback", .) and x])', $doc->documentElement));
+
+echo "in-scope namespace:\n";
+var_dump($xp->query('a[php:function("callback", .) and p:y]', $doc->documentElement)->length);
+
+echo "position and size:\n";
+var_dump($xp->query('a[php:function("callback", .) and position() = 2]', $doc->documentElement)->length);
+var_dump($xp->query('a[php:function("callback", .) and last() = 2]', $doc->documentElement)->length);
+
+echo "after reentry:\n";
+var_dump($xp->query('a[x]', $doc->documentElement)->length);
+
+echo "Dom\\XPath:\n";
+$modern = Dom\XMLDocument::createFromString('<r><a><x>A</x></a><a><x>A2</x></a><b><x>B</x><x>B2</x><x>B3</x></b></r>');
+$GLOBALS['xp'] = new Dom\XPath($modern);
+$GLOBALS['xp']->registerNamespace('php', 'http://php.net/xpath');
+$GLOBALS['xp']->registerPhpFunctions();
+$GLOBALS['b'] = $modern->documentElement->lastElementChild;
+var_dump($GLOBALS['xp']->query('a[php:function("callback", .) and x]', $modern->documentElement)->length);
+?>
+--EXPECT--
+context node:
+int(2)
+float(2)
+in-scope namespace:
+int(1)
+position and size:
+int(1)
+int(2)
+after reentry:
+int(2)
+Dom\XPath:
+int(2)
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index d4367c9aaa6..64161da640b 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -293,6 +293,10 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern)
 		RETURN_THROWS();
 	}

+	xmlNodePtr old_node = ctxp->node;
+	xmlNsPtr *old_namespaces = ctxp->namespaces;
+	int old_nsNr = ctxp->nsNr;
+
 	ctxp->node = nodep;

 	php_dom_in_scope_ns in_scope_ns;
@@ -310,12 +314,12 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern)
 	intern->evaluation_depth++;
 	xmlXPathObjectPtr xpathobjp = xmlXPathEvalExpression(BAD_CAST expr, ctxp);
 	intern->evaluation_depth--;
-	ctxp->node = NULL;
+	ctxp->node = old_node;
+	ctxp->namespaces = old_namespaces;
+	ctxp->nsNr = old_nsNr;

 	if (register_node_ns && nodep != NULL) {
 		php_dom_in_scope_ns_destroy(&in_scope_ns);
-		ctxp->namespaces = NULL;
-		ctxp->nsNr = 0;
 	}

 	if (! xpathobjp) {