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.