Commit dd576944029 for php.net

commit dd5769440296460502b3674f72a6be8d43769ac3
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Fri Jul 3 07:12:46 2026 -0400

    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

diff --git a/NEWS b/NEWS
index 6d4d1865d79..08a852a5a0d 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 e4d285c990f..9c269e4cb14 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 19f640e6b4a..e81ca5a7b23 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 00000000000..af78acf00df
--- /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 5e6a6f93b91..2d9e55a7b70 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: