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