Commit 11047a7a668 for php.net

commit 11047a7a6688b2360c3c3a26370caa39862a2587
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Mon Aug 10 08:43:15 2026 -0400

    Fix use-after-free when __clone() retains the stylesheet copy

    importStylesheet() clones the stylesheet document and hands the copy to
    libxslt, which owns it and frees it together with the stylesheet. The
    clone goes through zend_objects_clone_members(), so a DOMDocument
    subclass __clone() can retain the copy, or a node proxy into it, and
    dereference freed memory once the processor is destroyed. Require the
    clone to be exclusively owned before libxslt takes it.

    Closes GH-23199

diff --git a/NEWS b/NEWS
index d19e6b8ae2a..6447fa7bc88 100644
--- a/NEWS
+++ b/NEWS
@@ -87,6 +87,10 @@ PHP                                                                        NEWS
   . Fixed out-of-bounds write when shm_attach() opens an existing segment with
     a size larger than the segment actually is. (David Carlier)

+- XSL:
+  . Fixed use-after-free when a DOMDocument subclass __clone() retains the
+    stylesheet copy made by XSLTProcessor::importStylesheet(). (iliaal)
+
 - Zip:
   . Fixed ZipArchive::addGlob() and ZipArchive::addPattern() ignoring their
     default options when no options array is given. (David Carlier)
diff --git a/ext/xsl/tests/importStylesheet_clone_retained_document.phpt b/ext/xsl/tests/importStylesheet_clone_retained_document.phpt
new file mode 100644
index 00000000000..481925677d4
--- /dev/null
+++ b/ext/xsl/tests/importStylesheet_clone_retained_document.phpt
@@ -0,0 +1,47 @@
+--TEST--
+XSLTProcessor::importStylesheet() rejects a stylesheet whose __clone() retains the cloned document
+--EXTENSIONS--
+dom
+xsl
+--FILE--
+<?php
+const STYLESHEET = <<<XML
+<?xml version="1.0"?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+  <xsl:template match="/"><out/></xsl:template>
+</xsl:stylesheet>
+XML;
+
+class Harmless extends DOMDocument {
+    public function __clone(): void {
+    }
+}
+
+class RetainsDocument extends DOMDocument {
+    public function __clone(): void {
+        $GLOBALS['stash'] = $this;
+    }
+}
+
+$doc = new Harmless;
+$doc->loadXML(STYLESHEET);
+$proc = new XSLTProcessor();
+var_dump($proc->importStylesheet($doc));
+unset($proc, $doc);
+
+$doc = new RetainsDocument;
+$doc->loadXML(STYLESHEET);
+$proc = new XSLTProcessor();
+try {
+    var_dump($proc->importStylesheet($doc));
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+$kept = $GLOBALS['stash'];
+unset($GLOBALS['stash'], $proc, $doc);
+echo get_class($kept), " is still usable: ", $kept->documentElement->nodeName, PHP_EOL;
+?>
+--EXPECT--
+bool(true)
+ValueError: XSLTProcessor::importStylesheet(): Argument #1 ($stylesheet) must not have its clone retained by __clone()
+RetainsDocument is still usable: xsl:stylesheet
diff --git a/ext/xsl/tests/importStylesheet_clone_retained_node.phpt b/ext/xsl/tests/importStylesheet_clone_retained_node.phpt
new file mode 100644
index 00000000000..72c47b73b00
--- /dev/null
+++ b/ext/xsl/tests/importStylesheet_clone_retained_node.phpt
@@ -0,0 +1,34 @@
+--TEST--
+XSLTProcessor::importStylesheet() rejects a stylesheet whose __clone() retains a node of the cloned document
+--EXTENSIONS--
+dom
+xsl
+--FILE--
+<?php
+class RetainsElement extends DOMDocument {
+    public function __clone(): void {
+        $GLOBALS['stash'] = $this->documentElement;
+    }
+}
+
+$doc = new RetainsElement;
+$doc->loadXML(<<<XML
+<?xml version="1.0"?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+  <xsl:template match="/"><out/></xsl:template>
+</xsl:stylesheet>
+XML);
+
+$proc = new XSLTProcessor();
+try {
+    var_dump($proc->importStylesheet($doc));
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+$kept = $GLOBALS['stash'];
+unset($GLOBALS['stash'], $proc, $doc);
+echo get_class($kept), " is still usable: ", $kept->nodeName, PHP_EOL;
+?>
+--EXPECT--
+ValueError: XSLTProcessor::importStylesheet(): Argument #1 ($stylesheet) must not have its clone retained by __clone()
+DOMElement is still usable: xsl:stylesheet
diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c
index 71971332a25..cf5a941d95c 100644
--- a/ext/xsl/xsltprocessor.c
+++ b/ext/xsl/xsltprocessor.c
@@ -227,6 +227,12 @@ PHP_METHOD(XSLTProcessor, importStylesheet)

 	php_libxml_node_object *clone_lxml_obj = Z_LIBXML_NODE_P(&clone_zv);

+	if (GC_REFCOUNT(clone) > 1 || clone_lxml_obj->document->refcount > 1) {
+		OBJ_RELEASE(clone);
+		zend_argument_value_error(1, "must not have its clone retained by __clone()");
+		RETURN_THROWS();
+	}
+
 	PHP_LIBXML_SANITIZE_GLOBALS(parse);
 	ZEND_DIAGNOSTIC_IGNORED_START("-Wdeprecated-declarations")
 	xmlSubstituteEntitiesDefault(1);