Commit c4c356a7bac for php.net
commit c4c356a7bac4627bf22bc3deaff9d587be866484
Author: Weilin Du <weilindu@php.net>
Date: Sun Aug 9 00:04:06 2026 +0800
Fix GH-23120: DOMNode::isEqualNode stack overflow on deeply nested trees (#23140)
diff --git a/NEWS b/NEWS
index 81f448cbd56..e4a21b1bfa4 100644
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,8 @@ PHP NEWS
. Fixed bug GH-22447 (UAF at dom_objects_free_storage when setting an
attribute node that collides by local name with a namespaced
attribute). (David Carlier)
+ . Fixed bug GH-23120 (Stack overflow when comparing deeply nested DOM nodes
+ with DOMNode::isEqualNode()). (Weilin Du)
- MBString:
. Fixed bug GH-22779 (mb_strrpos() returns the wrong position for a negative
diff --git a/ext/dom/node.c b/ext/dom/node.c
index 81c80cb0c8a..a42dfedc32a 100644
--- a/ext/dom/node.c
+++ b/ext/dom/node.c
@@ -1730,11 +1730,27 @@ static bool php_dom_is_equal_attr(const xmlAttr *this_attr, const xmlAttr *other
&& php_dom_node_is_content_equal((const xmlNode *) this_attr, (const xmlNode *) other_attr);
}
+static zend_always_inline bool php_dom_node_is_equal_node_check_stack_limit(void)
+{
+#ifdef ZEND_CHECK_STACK_LIMIT
+ return zend_call_stack_overflowed(EG(stack_limit));
+#else
+ return false;
+#endif
+}
+
static bool php_dom_node_is_equal_node(const xmlNode *this, const xmlNode *other, bool spec_compliant)
{
ZEND_ASSERT(this != NULL);
ZEND_ASSERT(other != NULL);
+ if (UNEXPECTED(php_dom_node_is_equal_node_check_stack_limit())) {
+ if (!EG(exception)) {
+ zend_throw_error(NULL, "Maximum call stack size reached.");
+ }
+ return false;
+ }
+
if (this->type != other->type) {
return false;
}
@@ -1795,6 +1811,7 @@ static void dom_node_is_equal_node_common(INTERNAL_FUNCTION_PARAMETERS, bool mod
zval *id, *node;
xmlNodePtr otherp, nodep;
dom_object *intern;
+ bool result;
id = ZEND_THIS;
ZEND_PARSE_PARAMETERS_START(1, 1)
@@ -1817,7 +1834,11 @@ static void dom_node_is_equal_node_common(INTERNAL_FUNCTION_PARAMETERS, bool mod
RETURN_BOOL(nodep == NULL && otherp == NULL);
}
- RETURN_BOOL(php_dom_node_is_equal_node(nodep, otherp, modern));
+ result = php_dom_node_is_equal_node(nodep, otherp, modern);
+ if (UNEXPECTED(EG(exception))) {
+ RETURN_THROWS();
+ }
+ RETURN_BOOL(result);
}
PHP_METHOD(DOMNode, isEqualNode)
diff --git a/ext/dom/tests/gh23120.phpt b/ext/dom/tests/gh23120.phpt
new file mode 100644
index 00000000000..511314d9cbb
--- /dev/null
+++ b/ext/dom/tests/gh23120.phpt
@@ -0,0 +1,42 @@
+--TEST--
+GH-23120 (Stack overflow when comparing deeply nested DOM nodes)
+--EXTENSIONS--
+dom
+--SKIPIF--
+<?php
+if (ini_get('zend.max_allowed_stack_size') === false) {
+ die('skip No stack limit support');
+}
+if (getenv('SKIP_ASAN')) {
+ die('skip ASAN needs different stack limit setting due to more stack space usage');
+}
+?>
+--INI--
+zend.max_allowed_stack_size=512K
+--FILE--
+<?php
+function create_deep_document(): DOMDocument {
+ $doc = new DOMDocument();
+ $node = $doc->createElement('leaf', 'x');
+
+ for ($i = 0; $i < 10000; $i++) {
+ $parent = $doc->createElement('a');
+ $parent->appendChild($node);
+ $node = $parent;
+ }
+
+ $doc->appendChild($node);
+ return $doc;
+}
+
+$doc1 = create_deep_document();
+$doc2 = create_deep_document();
+
+try {
+ var_dump($doc1->documentElement->isEqualNode($doc2->documentElement));
+} catch (\Error $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Error: Maximum call stack size reached.