Commit f81f58c1719 for php.net

commit f81f58c17192f7385329c9050532971b363de50b
Author: lazerg <lazerg2@gmail.com>
Date:   Sat Aug 8 16:45:15 2026 +0800

    Fix GH-23116 and GH-23117: stack overflow when normalizing a deeply nested document (#23127)

    DOMNode::normalize() and Dom\Node::normalize() recurse once per element child,
    so a deeply nested document exhausts the C stack and segfaults. Both functions
    now check the stack limit on entry and throw an Error, matching the XML
    serializer behavior added in GH-22570.

    The throw is gated on EG(exception) because these functions return void. Without
    that guard, unwinding through a tree that is wide at the overflow depth throws
    once per node and chains the Errors, which is quadratic.

    Fixes GH-23116
    Fixes GH-23117

    Closes #23127

    Co-authored-by: David Carlier <devnexen@gmail.com>

diff --git a/NEWS b/NEWS
index ba832ef0588..81f448cbd56 100644
--- a/NEWS
+++ b/NEWS
@@ -6,6 +6,10 @@ PHP                                                                        NEWS
   . Fixed leak on double DatePeriod::__construct() call. (ilutov)

 - DOM:
+  . Fixed bug GH-23116 (Stack overflow when normalizing a deeply nested
+    DOMDocument). (Lazizbek Ergashev)
+  . Fixed bug GH-23117 (Stack overflow when normalizing a deeply nested
+    Dom\XMLDocument). (Lazizbek Ergashev)
   . Fixed bug GH-22825 (DOMElement::setAttribute() fails silently when the DTD
     declares a default value for the attribute). (iliaal)
   . Fixed bug GH-22447 (UAF at dom_objects_free_storage when setting an
diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c
index 135b3cdc5ca..00f63d47503 100644
--- a/ext/dom/php_dom.c
+++ b/ext/dom/php_dom.c
@@ -1966,9 +1966,26 @@ static void dom_merge_adjacent_exclusive_text_nodes(xmlNodePtr node)
 	}
 }

+static zend_always_inline bool dom_normalize_check_stack_limit(void)
+{
+#ifdef ZEND_CHECK_STACK_LIMIT
+	if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
+		if (!EG(exception)) {
+			zend_throw_error(NULL, "Maximum call stack size reached. Infinite recursion?");
+		}
+		return true;
+	}
+#endif
+	return false;
+}
+
 /* {{{ void php_dom_normalize_legacy(xmlNodePtr nodep) */
 void php_dom_normalize_legacy(xmlNodePtr nodep)
 {
+	if (UNEXPECTED(dom_normalize_check_stack_limit())) {
+		return;
+	}
+
 	xmlNodePtr child = nodep->children;
 	while(child != NULL) {
 		switch (child->type) {
@@ -2001,6 +2018,10 @@ void php_dom_normalize_legacy(xmlNodePtr nodep)
 /* https://dom.spec.whatwg.org/#dom-node-normalize */
 void php_dom_normalize_modern(xmlNodePtr this)
 {
+	if (UNEXPECTED(dom_normalize_check_stack_limit())) {
+		return;
+	}
+
 	/* for each descendant exclusive Text node node of this: */
 	xmlNodePtr node = this->children;
 	while (node != NULL) {
diff --git a/ext/dom/tests/gh23116.phpt b/ext/dom/tests/gh23116.phpt
new file mode 100644
index 00000000000..20cabc4c535
--- /dev/null
+++ b/ext/dom/tests/gh23116.phpt
@@ -0,0 +1,50 @@
+--TEST--
+GH-23116 (Stack overflow when normalizing a deeply nested DOMDocument)
+--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=256K
+--FILE--
+<?php
+// Build bottom-up so the insertion cycle-check stays O(1); top-down is O(n^2).
+$doc = new DOMDocument();
+$root = $doc->createElement('root');
+for ($s = 0; $s < 2; $s++) {
+    $node = $doc->createElement('a');
+    for ($i = 0; $i < 25000; $i++) {
+        $parent = $doc->createElement('a');
+        $parent->appendChild($node);
+        $node = $parent;
+    }
+    $root->appendChild($node);
+}
+$doc->appendChild($root);
+
+try {
+    $doc->normalize();
+} catch (\Error $e) {
+    echo "normalize: ", $e::class, ": ", $e->getMessage(), "\n";
+    var_dump($e->getPrevious());
+}
+
+try {
+    $doc->normalizeDocument();
+} catch (\Error $e) {
+    echo "normalizeDocument: ", $e::class, ": ", $e->getMessage(), "\n";
+    var_dump($e->getPrevious());
+}
+?>
+--EXPECT--
+normalize: Error: Maximum call stack size reached. Infinite recursion?
+NULL
+normalizeDocument: Error: Maximum call stack size reached. Infinite recursion?
+NULL
diff --git a/ext/dom/tests/modern/spec/gh23117.phpt b/ext/dom/tests/modern/spec/gh23117.phpt
new file mode 100644
index 00000000000..d284677799c
--- /dev/null
+++ b/ext/dom/tests/modern/spec/gh23117.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23117 (Stack overflow when normalizing a deeply nested Dom\XMLDocument)
+--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=256K
+--FILE--
+<?php
+// Build bottom-up so the insertion cycle-check stays O(1); top-down is O(n^2).
+$doc = Dom\XMLDocument::createEmpty();
+$root = $doc->createElement('root');
+for ($s = 0; $s < 2; $s++) {
+    $node = $doc->createElement('a');
+    for ($i = 0; $i < 25000; $i++) {
+        $parent = $doc->createElement('a');
+        $parent->appendChild($node);
+        $node = $parent;
+    }
+    $root->appendChild($node);
+}
+$doc->appendChild($root);
+
+try {
+    $doc->normalize();
+} catch (\Error $e) {
+    echo "normalize: ", $e::class, ": ", $e->getMessage(), "\n";
+    var_dump($e->getPrevious());
+}
+?>
+--EXPECT--
+normalize: Error: Maximum call stack size reached. Infinite recursion?
+NULL