[php-src] master: ext/dom: Fix GH-22522: Double-free of namespaces on OOM during parsing

David Carlier <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: David Carlier (devnexen)
Date: 2026-08-17T04:49:32+01:00

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

ext/dom: Fix GH-22522: Double-free of namespaces on OOM during parsing

php_dom_ns_compat_mark_attribute_list() cleared node->nsDef only after
moving every namespace onto doc->oldNs, so a bailout mid-loop (e.g. OOM)
left both lists owning the same namespaces, double-freeing them on
teardown. Move each namespace to oldNs before the allocating calls so it
is only ever owned by one list.

Close GH-22539

Changed paths:
  A  ext/dom/tests/modern/xml/gh22522.phpt
  M  ext/dom/namespace_compat.c


Diff:

diff --git a/ext/dom/namespace_compat.c b/ext/dom/namespace_compat.c
index 1815b2dd4413..29a0714c62fc 100644
--- a/ext/dom/namespace_compat.c
+++ b/ext/dom/namespace_compat.c
@@ -240,32 +240,34 @@ PHP_DOM_EXPORT void php_dom_ns_compat_mark_attribute_list(php_dom_libxml_ns_mapp
 
 	/* We want to prepend at the front, but in order of the namespace definitions.
 	 * So temporarily unlink the existing properties and add them again at the end. */
-	xmlAttrPtr attr = node->properties;
-	node->properties = NULL;
+	xmlAttrPtr first_original = node->properties;
+	xmlAttrPtr first_ns_attr = NULL, last_ns_attr = NULL;
 
 	xmlNsPtr ns = node->nsDef;
-	xmlAttrPtr last_added = NULL;
 	do {
-		last_added = php_dom_ns_compat_mark_attribute(mapper, node, ns);
-		php_dom_libxml_ns_mapper_store_and_normalize_parsed_ns(mapper, ns);
 		xmlNsPtr next = ns->next;
+		node->nsDef = next;
 		ns->next = NULL;
 		php_libxml_set_old_ns(node->doc, ns);
+		xmlAttrPtr added = php_dom_ns_compat_mark_attribute(mapper, node, ns);
+		if (added != NULL) {
+			if (first_ns_attr == NULL) {
+				first_ns_attr = added;
+			}
+			last_ns_attr = added;
+		}
+		php_dom_libxml_ns_mapper_store_and_normalize_parsed_ns(mapper, ns);
 		ns = next;
 	} while (ns != NULL);
 
-	if (last_added != NULL) {
-		/* node->properties now points to the first namespace declaration attribute. */
-		if (attr != NULL) {
-			last_added->next = attr;
-			attr->prev = last_added;
-		}
-	} else {
-		/* Nothing added, so nothing changed. Only really possible on OOM. */
-		node->properties = attr;
+	if (first_ns_attr != NULL && first_original != NULL) {
+		xmlAttrPtr last_original = first_ns_attr->prev;
+		last_original->next = NULL;
+		first_ns_attr->prev = NULL;
+		last_ns_attr->next = first_original;
+		first_original->prev = last_ns_attr;
+		node->properties = first_ns_attr;
 	}
-
-	node->nsDef = NULL;
 }
 
 PHP_DOM_EXPORT bool php_dom_ns_is_fast_ex(xmlNsPtr ns, const php_dom_ns_magic_token *magic_token)
diff --git a/ext/dom/tests/modern/xml/gh22522.phpt b/ext/dom/tests/modern/xml/gh22522.phpt
new file mode 100644
index 000000000000..4024de21ee8c
--- /dev/null
+++ b/ext/dom/tests/modern/xml/gh22522.phpt
@@ -0,0 +1,46 @@
+--TEST--
+GH-22522 (Double-free of namespaces when re-parsing while iterating in-scope namespaces)
+--CREDITS--
+Yuancheng Jiang (YuanchengJiang)
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+
+$xml = <<<XML
+<root xmlns="urn:a">
+    <child xmlns="">
+        <c:child xmlns:c="urn:c"/>
+    </child>
+    <b:sibling xmlns:b="urn:b" xmlns:d="urn:d" d:foo="bar">
+        <d:child xmlns:d="urn:d2"/>
+    </b:sibling>
+</root>
+XML;
+
+function dump($dom, $name, $depth) {
+    $list = $dom->getElementsByTagName($name)[0]->getInScopeNamespaces();
+    foreach ($list as $entry) {
+        // Re-parse namespace-heavy markup while a snapshot of the in-scope
+        // namespaces is still alive; used to double-free on teardown.
+        $reparsed = Dom\XMLDocument::createFromString($GLOBALS['xml']);
+        if ($depth > 0) {
+            dump($reparsed, 'c:child', $depth - 1);
+            dump($reparsed, 'child', $depth - 1);
+        }
+        echo $entry->prefix, " => ", $entry->namespaceURI, "\n";
+    }
+}
+
+$dom = Dom\XMLDocument::createFromString($xml);
+dump($dom, 'c:child', 3);
+dump($dom, 'child', 3);
+echo "done\n";
+
+?>
+--EXPECT--
+c => urn:c
+c => urn:c
+c => urn:c
+c => urn:c
+done
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.