[php-src] PHP-8.4: Fix use-after-free in XPath php:function argument nodes

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-06-27T08:21:54-04:00

Commit: https://github.com/php/php-src/commit/f9821dd1dda7941a4528aef0529b2a454e6da792
Raw diff: https://github.com/php/php-src/commit/f9821dd1dda7941a4528aef0529b2a454e6da792.diff

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

Changed paths:
  A  ext/dom/tests/xpath_php_function_removes_argument_node.phpt
  M  ext/dom/xpath_callbacks.c


Diff:

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 000000000000..e6696617be25
--- /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 28a5272a302a..53e8f3443149 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(&params[i]);
+			zval *param = &params[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 *) ""));
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.