[php-src] PHP-8.4: Fix GH-23116 and GH-23117: stack overflow when normalizing a deeply nested document (#23127)

lazerg via Weilin Du <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: lazerg (lazerg)
Committer: Weilin Du (LamentXU123)
Date: 2026-08-08T16:51:30+08:00

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

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 <[email protected]>

Changed paths:
  A  ext/dom/tests/gh23116.phpt
  A  ext/dom/tests/modern/spec/gh23117.phpt
  M  NEWS
  M  ext/dom/php_dom.c


Diff:

diff --git a/NEWS b/NEWS
index ba832ef05888..81f448cbd56e 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 135b3cdc5caa..00f63d475032 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 000000000000..20cabc4c535a
--- /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 000000000000..d284677799cd
--- /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
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.