[php-src] PHP-8.4: Fix GH-22570: stack overflow serializing a deeply nested Dom\XMLDocument

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-07-03T13:53:38-04:00

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

Fix GH-22570: stack overflow serializing a deeply nested Dom\XMLDocument

The new-DOM XML serializer recurses through dom_xml_serialization_algorithm()
for every element child, so a document nested deeply enough overflows the C
stack and crashes during saveXml() or innerHTML. Add a stack-limit check at
the dispatcher, throwing an Error on overflow, mirroring bd724bd. Gate the
"Could not save document" warning and the innerHTML not-well-formed exception
with !EG(exception) so the thrown Error propagates cleanly instead of being
accompanied by a warning or replaced with the wrong exception type.

Fixes GH-22570
Closes GH-22576

Changed paths:
  A  ext/dom/tests/modern/xml/gh22570.phpt
  M  NEWS
  M  ext/dom/document.c
  M  ext/dom/inner_html_mixin.c
  M  ext/dom/xml_serializer.c


Diff:

diff --git a/NEWS b/NEWS
index 6d4d1865d790..08a852a5a0d8 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,10 @@ PHP                                                                        NEWS
 - DBA:
   . Fixed OOB read on malformed length field in dba flatfile handler. (alhudz)
 
+- DOM:
+  . Fixed bug GH-22570 (Stack overflow when serializing a deeply nested
+    Dom\XMLDocument). (iliaal)
+
 - Exif:
   . Fixed bug GH-11020 (exif_read_data() emits a spurious "Illegal IFD size"
     warning when an IFD is not followed by a next-IFD offset). (Eyüp Can Akman)
diff --git a/ext/dom/document.c b/ext/dom/document.c
index e4d285c990fe..9c269e4cb14d 100644
--- a/ext/dom/document.c
+++ b/ext/dom/document.c
@@ -1678,7 +1678,9 @@ static void dom_document_save_xml(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry
 	}
 
 	if (!res) {
-		php_error_docref(NULL, E_WARNING, "Could not save document");
+		if (!EG(exception)) {
+			php_error_docref(NULL, E_WARNING, "Could not save document");
+		}
 		RETURN_FALSE;
 	} else {
 		RETURN_NEW_STR(res);
diff --git a/ext/dom/inner_html_mixin.c b/ext/dom/inner_html_mixin.c
index 19f640e6b4ac..e81ca5a7b238 100644
--- a/ext/dom/inner_html_mixin.c
+++ b/ext/dom/inner_html_mixin.c
@@ -103,7 +103,9 @@ zend_result dom_element_inner_html_read(dom_object *obj, zval *retval)
 		}
 		if (UNEXPECTED(status < 0)) {
 			smart_str_free_ex(&str, false);
-			php_dom_throw_error_with_message(SYNTAX_ERR, "The resulting XML serialization is not well-formed", true);
+			if (!EG(exception)) {
+				php_dom_throw_error_with_message(SYNTAX_ERR, "The resulting XML serialization is not well-formed", true);
+			}
 			return FAILURE;
 		}
 		ZVAL_STR(retval, smart_str_extract(&str));
diff --git a/ext/dom/tests/modern/xml/gh22570.phpt b/ext/dom/tests/modern/xml/gh22570.phpt
new file mode 100644
index 000000000000..af78acf00dfa
--- /dev/null
+++ b/ext/dom/tests/modern/xml/gh22570.phpt
@@ -0,0 +1,40 @@
+--TEST--
+GH-22570 (Stack overflow when serializing 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=512K
+--FILE--
+<?php
+// Build via the DOM API, not the parser: libxml caps parse depth even with
+// LIBXML_PARSEHUGE on some platforms; the serializer recursion is the bug.
+$doc = Dom\XMLDocument::createEmpty();
+$node = $doc->appendChild($doc->createElement('root'));
+for ($i = 0; $i < 100000; $i++) {
+    $node = $node->appendChild($doc->createElement('a'));
+}
+
+try {
+    $doc->saveXml();
+} catch (\Error $e) {
+    echo "saveXml: ", $e::class, ": ", $e->getMessage(), "\n";
+}
+
+try {
+    $doc->documentElement->innerHTML;
+} catch (\Error $e) {
+    echo "innerHTML: ", $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+saveXml: Error: Maximum call stack size reached. Infinite recursion?
+innerHTML: Error: Maximum call stack size reached. Infinite recursion?
diff --git a/ext/dom/xml_serializer.c b/ext/dom/xml_serializer.c
index 5e6a6f93b91c..2d9e55a7b706 100644
--- a/ext/dom/xml_serializer.c
+++ b/ext/dom/xml_serializer.c
@@ -1250,6 +1250,15 @@ static int dom_xml_serializing_a_document_node(
 	return 0;
 }
 
+static zend_always_inline bool dom_xml_serialize_check_stack_limit(void)
+{
+#ifdef ZEND_CHECK_STACK_LIMIT
+	return zend_call_stack_overflowed(EG(stack_limit));
+#else
+	return false;
+#endif
+}
+
 /* https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization-algorithm */
 static int dom_xml_serialization_algorithm(
 	dom_xml_serialize_ctx *ctx,
@@ -1261,6 +1270,11 @@ static int dom_xml_serialization_algorithm(
 	bool require_well_formed
 )
 {
+	if (UNEXPECTED(dom_xml_serialize_check_stack_limit())) {
+		zend_throw_error(NULL, "Maximum call stack size reached. Infinite recursion?");
+		return -1;
+	}
+
 	/* If node's interface is: */
 	switch (node->type) {
 		case XML_ELEMENT_NODE:
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.