Commit 0dd7f31f151 for php.net

commit 0dd7f31f151636ecab5ef668087693ae266895ce
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Mon Aug 17 19:56:11 2026 -0400

    Fix UAF when setAttributeNS() frees a wrapped attribute child

    dom_set_attribute_ns_modern() handed the element straight to
    xmlSetNsProp(), which frees the existing attribute's child list, so a
    live Dom\Text wrapper for one of those children was left pointing at
    freed memory. Unlink the wrapped children first, as
    dom_set_attribute_ns_legacy() already does.

    Closes GH-23348

diff --git a/NEWS b/NEWS
index 182c80517b0..d9f2e25bcdf 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,8 @@ PHP                                                                        NEWS
     DOMDocument::xinclude(). (iliaal)
   . Fixed bug GH-23331 (UAF when node_list_unlink() skips attribute children
     that still have a live wrapper). (iliaal)
+  . Fixed a use-after-free when Dom\Element::setAttributeNS() replaces the
+    value of an attribute whose child still has a live wrapper. (iliaal)

 - Intl:
   . Fixed a double-free when IntlGregorianCalendar construction fails after
diff --git a/ext/dom/element.c b/ext/dom/element.c
index 3bce1bdac2a..8af5d54db33 100644
--- a/ext/dom/element.c
+++ b/ext/dom/element.c
@@ -1030,6 +1030,10 @@ static void dom_set_attribute_ns_modern(dom_object *intern, xmlNodePtr elemp, ze
 	if (errorcode == 0) {
 		php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(intern);
 		xmlNsPtr ns = php_dom_libxml_ns_mapper_get_ns_raw_prefix_string(ns_mapper, prefix, xmlStrlen(prefix), uri);
+		xmlNodePtr existing = (xmlNodePtr) xmlHasNsProp(elemp, localname, ns == NULL ? NULL : ns->href);
+		if (existing != NULL && existing->type != XML_ATTRIBUTE_DECL) {
+			node_list_unlink(existing->children);
+		}
 		xmlAttrPtr attr = xmlSetNsProp(elemp, ns, localname, BAD_CAST value);
 		if (UNEXPECTED(attr == NULL)) {
 			php_dom_throw_error(INVALID_STATE_ERR, /* strict */ true);
diff --git a/ext/dom/tests/modern/common/Element_setAttributeNS_live_child.phpt b/ext/dom/tests/modern/common/Element_setAttributeNS_live_child.phpt
new file mode 100644
index 00000000000..22475c33cf8
--- /dev/null
+++ b/ext/dom/tests/modern/common/Element_setAttributeNS_live_child.phpt
@@ -0,0 +1,37 @@
+--TEST--
+setAttributeNS() keeps an attribute child that still has a live wrapper
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+
+$doc = Dom\XMLDocument::createFromString('<root xmlns:p="urn:x" p:attr="old"/>');
+$el = $doc->documentElement;
+$text = $el->getAttributeNodeNS('urn:x', 'attr')->firstChild;
+$el->setAttributeNS('urn:x', 'p:attr', 'new');
+echo "prefixed, detached: ";
+var_dump($text->parentNode === null);
+echo "prefixed, text: ";
+var_dump($text->textContent);
+echo "prefixed, new value: ";
+var_dump($el->getAttributeNS('urn:x', 'attr'));
+
+$doc = Dom\XMLDocument::createFromString('<root attr="old"/>');
+$el = $doc->documentElement;
+$text = $el->getAttributeNode('attr')->firstChild;
+$el->setAttributeNS(null, 'attr', 'new');
+echo "no namespace, detached: ";
+var_dump($text->parentNode === null);
+echo "no namespace, text: ";
+var_dump($text->textContent);
+echo "no namespace, new value: ";
+var_dump($el->getAttribute('attr'));
+
+?>
+--EXPECT--
+prefixed, detached: bool(true)
+prefixed, text: string(3) "old"
+prefixed, new value: string(3) "new"
+no namespace, detached: bool(true)
+no namespace, text: string(3) "old"
+no namespace, new value: string(3) "new"