Commit 9073875eb98 for php.net

commit 9073875eb98416595f1666df0f6a728bab2bc5c7
Author: David Carlier <devnexen@gmail.com>
Date:   Mon Jul 6 23:25:57 2026 +0100

    ext/dom: Use-after-free with namespace nodes from XSLTProcessor::registerPHPFunctions().

    Fix #22621

    Namespace parents from an external document() were wrapped directly
    instead of through the proxy factory, so they skipped the copy that
    keeps non-main-document nodes alive. A DOMNameSpaceNode retained by
    userland then dangled once xsltFreeTransformContext() freed the external
    document. Route the namespace parent through the proxy factory too.

    close GH-22623

diff --git a/NEWS b/NEWS
index cd259f06660..f40c6dfd8da 100644
--- a/NEWS
+++ b/NEWS
@@ -33,6 +33,8 @@ PHP                                                                        NEWS
   . Fixed Dom\DtdNamedNodeMap integer dimension access so negative indexes
     return NULL and indexes outside the int range throw ValueError instead of
     returning the first entity or notation. (Weilin Du)
+  . Fixed bug GH-22623 (use after free with namespace nodes from
+    XSLTProcessor::registerFunctions())/ (David Carlier)

 - Exif:
   . Fixed bug GH-11020 (exif_read_data() emits a spurious "Illegal IFD size"
diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c
index 622dd187dd8..349c304c9f2 100644
--- a/ext/dom/xpath_callbacks.c
+++ b/ext/dom/xpath_callbacks.c
@@ -347,15 +347,15 @@ static zval *php_dom_xpath_callback_fetch_args(xmlXPathParserContextPtr ctxt, ui
 							xmlNodePtr node = obj->nodesetval->nodeTab[j];
 							zval child;
 							if (UNEXPECTED(node->type == XML_NAMESPACE_DECL)) {
-								xmlNodePtr nsparent = node->_private;
 								xmlNsPtr original = (xmlNsPtr) node;

 								/* Make sure parent dom object exists, so we can take an extra reference. */
 								zval parent_zval; /* don't destroy me, my lifetime is transferred to the fake namespace decl */
-								php_dom_create_object(nsparent, &parent_zval, intern);
+								proxy_factory(node->_private, &parent_zval, intern, ctxt);
 								dom_object *parent_intern = Z_DOMOBJ_P(&parent_zval);
+								xmlNodePtr parent = dom_object_get_node(parent_intern);

-								php_dom_create_fake_namespace_decl(nsparent, original, &child, parent_intern);
+								php_dom_create_fake_namespace_decl(parent, original, &child, parent_intern);
 							} else {
 								proxy_factory(node, &child, intern, ctxt);
 							}
diff --git a/ext/xsl/tests/gh22621.phpt b/ext/xsl/tests/gh22621.phpt
new file mode 100644
index 00000000000..b31671eff63
--- /dev/null
+++ b/ext/xsl/tests/gh22621.phpt
@@ -0,0 +1,63 @@
+--TEST--
+GH-22621 (UAF via XSLTProcessor::registerPHPFunctions() retaining namespace nodes from an external document)
+--EXTENSIONS--
+dom
+xsl
+--CREDITS--
+ExPatch-LLC
+--FILE--
+<?php
+$cDIR = __DIR__;
+file_put_contents($cDIR . '/gh22621_external.xml', '<root xmlns:custom="urn:custom"><data>secret</data></root>');
+$uri = 'file:///' . ltrim(str_replace('\\', '/', $cDIR), '/') . '/gh22621_external.xml';
+
+$stored_ns = null;
+
+function capture_ns($nodes) {
+    global $stored_ns;
+    foreach ($nodes as $node) {
+        if ($node instanceof DOMNameSpaceNode) {
+            $stored_ns = $node;
+        }
+    }
+    return "captured";
+}
+
+$xsl = new DOMDocument();
+$xsl->loadXML(<<<XSL
+<?xml version="1.0"?>
+<xsl:stylesheet version="1.0"
+    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+    xmlns:php="http://php.net/xsl">
+  <xsl:template match="/">
+    <result>
+      <xsl:value-of select="php:function('capture_ns', document('$uri')/*/namespace::*)"/>
+    </result>
+  </xsl:template>
+</xsl:stylesheet>
+XSL);
+
+$doc = new DOMDocument();
+$doc->loadXML('<input/>');
+
+$proc = new XSLTProcessor();
+$proc->registerPHPFunctions();
+$proc->importStylesheet($xsl);
+echo $proc->transformToXml($doc);
+
+var_dump($stored_ns->localName);
+var_dump($stored_ns->namespaceURI);
+var_dump($stored_ns->parentNode->localName);
+var_dump($stored_ns->ownerDocument instanceof DOMDocument);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh22621_external.xml');
+?>
+--EXPECTF--
+<?xml version="1.0"?>
+<result xmlns:php="http://php.net/xsl">captured</result>
+string(6) "custom"
+string(10) "urn:custom"
+string(4) "root"
+bool(true)