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: