diff --git a/ext/dom/namespace_compat.c b/ext/dom/namespace_compat.c
index 1815b2dd4413..0fcacffe42ef 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 (UNEXPECTED(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--
+
+
+
+
+
+
+
+
+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