Commit 50de982d31b for php.net

commit 50de982d31b785aa267cff461c02142945f82055
Author: David Carlier <devnexen@gmail.com>
Date:   Mon Aug 17 04:49:32 2026 +0100

    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

diff --git a/ext/dom/namespace_compat.c b/ext/dom/namespace_compat.c
index 1815b2dd441..29a0714c62f 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 00000000000..4024de21ee8
--- /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