Commit f9821dd1dda for php.net
commit f9821dd1dda7941a4528aef0529b2a454e6da792
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Fri Jun 26 08:15:14 2026 -0400
Fix use-after-free in XPath php:function argument nodes
A php:function() callback receives DOM node arguments as proxies that own
the underlying libxml node. If the callback detaches such a node, the
argument cleanup frees it while libxml is still evaluating the expression
and still references it in the result node-set. Keep node and node-set
argument proxies alive until evaluation ends, as returned nodes already are.
Closes GH-22468
diff --git a/ext/dom/tests/xpath_php_function_removes_argument_node.phpt b/ext/dom/tests/xpath_php_function_removes_argument_node.phpt
new file mode 100644
index 00000000000..e6696617be2
--- /dev/null
+++ b/ext/dom/tests/xpath_php_function_removes_argument_node.phpt
@@ -0,0 +1,33 @@
+--TEST--
+DOMXPath: a php:function callback that removes its argument node must not free it mid-evaluation
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+$doc = new DOMDocument();
+$doc->loadXML('<root><a>1</a><a>2</a><a>3</a><a>4</a></root>');
+$xp = new DOMXPath($doc);
+$xp->registerNamespace('php', 'http://php.net/xpath');
+$xp->registerPhpFunctions();
+
+function cb($nodes) {
+ foreach ($nodes as $n) {
+ if ($n->parentNode) {
+ $n->parentNode->removeChild($n);
+ }
+ }
+ return true;
+}
+
+$res = $xp->query('//a[php:function("cb", .)]');
+foreach ($res as $r) {
+ var_dump($r->nodeName);
+}
+echo "done\n";
+?>
+--EXPECT--
+string(1) "a"
+string(1) "a"
+string(1) "a"
+string(1) "a"
+done
diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c
index 28a5272a302..53e8f344314 100644
--- a/ext/dom/xpath_callbacks.c
+++ b/ext/dom/xpath_callbacks.c
@@ -381,11 +381,19 @@ static zval *php_dom_xpath_callback_fetch_args(xmlXPathParserContextPtr ctxt, ui
return params;
}
-static void php_dom_xpath_callback_cleanup_args(zval *params, uint32_t param_count)
+static void php_dom_xpath_callback_cleanup_args(php_dom_xpath_callbacks *xpath_callbacks, zval *params, uint32_t param_count)
{
if (params) {
for (uint32_t i = 0; i < param_count; i++) {
- zval_ptr_dtor(¶ms[i]);
+ zval *param = ¶ms[i];
+ if (Z_TYPE_P(param) == IS_OBJECT || Z_TYPE_P(param) == IS_ARRAY) {
+ if (xpath_callbacks->node_list == NULL) {
+ xpath_callbacks->node_list = zend_new_array(0);
+ }
+ zend_hash_next_index_insert_new(xpath_callbacks->node_list, param);
+ } else {
+ zval_ptr_dtor(param);
+ }
}
efree(params);
}
@@ -483,7 +491,7 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call_php_ns(php_dom_xpath_cal
cleanup:
xmlXPathFreeObject(obj);
- php_dom_xpath_callback_cleanup_args(params, param_count);
+ php_dom_xpath_callback_cleanup_args(xpath_callbacks, params, param_count);
cleanup_no_obj:
if (UNEXPECTED(result != SUCCESS)) {
/* Push sentinel value */
@@ -511,7 +519,7 @@ PHP_DOM_EXPORT zend_result php_dom_xpath_callbacks_call_custom_ns(php_dom_xpath_
zend_result result = php_dom_xpath_callback_dispatch(xpath_callbacks, ns, ctxt, params, param_count, function_name, function_name_length);
- php_dom_xpath_callback_cleanup_args(params, param_count);
+ php_dom_xpath_callback_cleanup_args(xpath_callbacks, params, param_count);
if (UNEXPECTED(result != SUCCESS)) {
/* Push sentinel value */
valuePush(ctxt, xmlXPathNewString((const xmlChar *) ""));