From 86d9b4a5e1ae248ffe20c549c47025f160f82985 Mon Sep 17 00:00:00 2001 From: lazerg Date: Sat, 8 Aug 2026 09:57:16 +0500 Subject: [PATCH 1/2] Fix GH-23116 and GH-23117: stack overflow when normalizing a deeply nested document --- NEWS | 4 +++ ext/dom/php_dom.c | 21 ++++++++++++++++ ext/dom/tests/gh23116.phpt | 35 ++++++++++++++++++++++++++ ext/dom/tests/modern/spec/gh23117.phpt | 27 ++++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 ext/dom/tests/gh23116.phpt create mode 100644 ext/dom/tests/modern/spec/gh23117.phpt 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..7fe09105bf0b --- /dev/null +++ b/ext/dom/tests/gh23116.phpt @@ -0,0 +1,35 @@ +--TEST-- +GH-23116 (Stack overflow when normalizing a deeply nested DOMDocument) +--EXTENSIONS-- +dom +--SKIPIF-- + +--INI-- +zend.max_allowed_stack_size=512K +--FILE-- +loadXML(str_repeat('', 100000) . 'x' . str_repeat('', 100000), LIBXML_PARSEHUGE); + +try { + $doc->normalize(); +} catch (\Error $e) { + echo "normalize: ", $e::class, ": ", $e->getMessage(), "\n"; +} + +try { + $doc->normalizeDocument(); +} catch (\Error $e) { + echo "normalizeDocument: ", $e::class, ": ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +normalize: Error: Maximum call stack size reached. Infinite recursion? +normalizeDocument: Error: Maximum call stack size reached. Infinite recursion? diff --git a/ext/dom/tests/modern/spec/gh23117.phpt b/ext/dom/tests/modern/spec/gh23117.phpt new file mode 100644 index 000000000000..de84a369d2ae --- /dev/null +++ b/ext/dom/tests/modern/spec/gh23117.phpt @@ -0,0 +1,27 @@ +--TEST-- +GH-23117 (Stack overflow when normalizing a deeply nested Dom\XMLDocument) +--EXTENSIONS-- +dom +--SKIPIF-- + +--INI-- +zend.max_allowed_stack_size=512K +--FILE-- +', 100000) . 'x' . str_repeat('', 100000), LIBXML_PARSEHUGE); + +try { + $doc->normalize(); +} catch (\Error $e) { + echo "normalize: ", $e::class, ": ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +normalize: Error: Maximum call stack size reached. Infinite recursion? From 8fb1d7c1d52d32704e5458609d0551e7f319c215 Mon Sep 17 00:00:00 2001 From: lazerg Date: Sat, 8 Aug 2026 10:42:53 +0500 Subject: [PATCH 2/2] Cover the sibling-branch case in the GH-23116 and GH-23117 tests --- ext/dom/tests/gh23116.phpt | 19 +++++++++++++++++-- ext/dom/tests/modern/spec/gh23117.phpt | 18 ++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/ext/dom/tests/gh23116.phpt b/ext/dom/tests/gh23116.phpt index 7fe09105bf0b..20cabc4c535a 100644 --- a/ext/dom/tests/gh23116.phpt +++ b/ext/dom/tests/gh23116.phpt @@ -12,24 +12,39 @@ if (getenv('SKIP_ASAN')) { } ?> --INI-- -zend.max_allowed_stack_size=512K +zend.max_allowed_stack_size=256K --FILE-- loadXML(str_repeat('', 100000) . 'x' . str_repeat('', 100000), LIBXML_PARSEHUGE); +$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 index de84a369d2ae..d284677799cd 100644 --- a/ext/dom/tests/modern/spec/gh23117.phpt +++ b/ext/dom/tests/modern/spec/gh23117.phpt @@ -12,16 +12,30 @@ if (getenv('SKIP_ASAN')) { } ?> --INI-- -zend.max_allowed_stack_size=512K +zend.max_allowed_stack_size=256K --FILE-- ', 100000) . 'x' . str_repeat('', 100000), LIBXML_PARSEHUGE); +// 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