Commit a74f826ff29 for php.net
commit a74f826ff2979a4d46d7c69b78fbb86c0f173373
Author: David Carlier <devnexen@gmail.com>
Date: Thu Sep 17 19:40:13 2026 +0100
Fix GH-23729: DOMXPath::__construct() use-after-free during an evaluation
Reconstructing the object from a php:function callback freed the context
libxml2 was still evaluating, and php_xpath_eval() wrote back into it once
the evaluation returned. The evaluation depth is now tracked on the object
and __construct() throws while it is non-zero.
Close GH-23735
diff --git a/NEWS b/NEWS
index c8eb609cc2a..46824ac7d6a 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,8 @@ PHP NEWS
. Fixed Dom\HTMLDocument giving attributes the namespace of their element
when a fragment is parsed with an xlink, xml or xmlns context element.
(Ilia Alshanetsky)
+ . Fixed bug GH-23729 (DOMXPath::__construct() use-after-free during an
+ evaluation). (David Carlier)
- Intl:
. Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state
diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h
index 13f49879bb3..d399d745084 100644
--- a/ext/dom/php_dom.h
+++ b/ext/dom/php_dom.h
@@ -66,6 +66,7 @@ extern zend_module_entry dom_module_entry;
typedef struct dom_xpath_object {
php_dom_xpath_callbacks xpath_callbacks;
+ uint32_t evaluation_depth;
bool register_node_ns;
dom_object dom;
} dom_xpath_object;
diff --git a/ext/dom/tests/gh23729.phpt b/ext/dom/tests/gh23729.phpt
new file mode 100644
index 00000000000..93ad67fde00
--- /dev/null
+++ b/ext/dom/tests/gh23729.phpt
@@ -0,0 +1,73 @@
+--TEST--
+GH-23729 (Use-after-free when DOMXPath is reconstructed during an evaluation)
+--CREDITS--
+djarfluka
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+
+function reconstruct() {
+ try {
+ $GLOBALS['xpath']->__construct($GLOBALS['other']);
+ } catch (Error $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+ }
+ return 'r';
+}
+
+function nested() {
+ echo 'nested: ', $GLOBALS['xpath']->evaluate('string(/root/b)'), PHP_EOL;
+ return 'n';
+}
+
+function test(string $class, object $doc, object $other) {
+ $xpath = new $class($doc);
+ $xpath->registerNamespace('php', 'http://php.net/xpath');
+ $xpath->registerPhpFunctions();
+
+ $GLOBALS['xpath'] = $xpath;
+ $GLOBALS['other'] = $other;
+
+ var_dump($xpath->evaluate('string(php:function("reconstruct"))'));
+ /* The evaluation the callback tried to destroy must still be usable. */
+ var_dump($xpath->evaluate('string(/root/a)'));
+ /* A nested evaluation must not lift the guard of the outer one. */
+ var_dump($xpath->evaluate('concat(php:function("nested"), php:function("reconstruct"))'));
+ var_dump($xpath->query('//b[php:function("reconstruct")]')->length);
+
+ /* Reconstructing outside of an evaluation is still allowed. */
+ $xpath->__construct($other);
+ var_dump($xpath->document->documentElement->nodeName);
+}
+
+$doc = new DOMDocument();
+$doc->loadXML('<root><a>1</a><b>2</b></root>');
+$other = new DOMDocument();
+$other->loadXML('<other/>');
+test(DOMXPath::class, $doc, $other);
+
+$doc = Dom\XMLDocument::createFromString('<root><a>1</a><b>2</b></root>');
+$other = Dom\XMLDocument::createFromString('<other/>');
+test(Dom\XPath::class, $doc, $other);
+
+?>
+--EXPECT--
+Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress
+string(1) "r"
+string(1) "1"
+nested: 2
+Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress
+string(2) "nr"
+Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress
+int(1)
+string(5) "other"
+Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress
+string(1) "r"
+string(1) "1"
+nested: 2
+Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress
+string(2) "nr"
+Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress
+int(1)
+string(5) "other"
diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c
index 4f1b3b52714..d4367c9aaa6 100644
--- a/ext/dom/xpath.c
+++ b/ext/dom/xpath.c
@@ -126,6 +126,13 @@ static void dom_xpath_construct(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry *
RETURN_THROWS();
}
+ dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
+ if (UNEXPECTED(intern->evaluation_depth > 0)) {
+ zend_throw_error(NULL, "Cannot call %s::__construct() while an XPath evaluation is in progress",
+ ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name));
+ RETURN_THROWS();
+ }
+
DOM_GET_OBJ(docp, doc, xmlDocPtr, docobj);
xmlXPathContextPtr ctx = xmlXPathNewContext(docp);
@@ -134,7 +141,6 @@ static void dom_xpath_construct(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry *
RETURN_THROWS();
}
- dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS);
xmlXPathContextPtr oldctx = intern->dom.ptr;
if (oldctx != NULL) {
php_libxml_decrement_doc_ref((php_libxml_node_object *) &intern->dom);
@@ -301,7 +307,9 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern)
ctxp->nsNr = in_scope_ns.count;
}
+ intern->evaluation_depth++;
xmlXPathObjectPtr xpathobjp = xmlXPathEvalExpression(BAD_CAST expr, ctxp);
+ intern->evaluation_depth--;
ctxp->node = NULL;
if (register_node_ns && nodep != NULL) {