[php-src] master: Merge branch 'PHP-8.5'

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-08-10T15:15:51-04:00

Commit: https://github.com/php/php-src/commit/c8682fb61af207c09c71bcded7fe5ba7b17d4a5e
Raw diff: https://github.com/php/php-src/commit/c8682fb61af207c09c71bcded7fe5ba7b17d4a5e.diff

Merge branch 'PHP-8.5'

* PHP-8.5:
  Fix use-after-free when __clone() retains the stylesheet copy

Changed paths:
  A  ext/xsl/tests/importStylesheet_clone_retained_document.phpt
  A  ext/xsl/tests/importStylesheet_clone_retained_node.phpt
  M  NEWS
  M  ext/xsl/xsltprocessor.c


Diff:

diff --git a/NEWS b/NEWS
index 7bbc50fcccfe..4ed01591ea4c 100644
--- a/NEWS
+++ b/NEWS
@@ -108,6 +108,10 @@ PHP                                                                        NEWS
   . Io\Poll\Context::wait() now takes a Time\Duration object as a timeout.
     (timwolla)
 
+- XSL:
+  . Fixed use-after-free when a DOMDocument subclass __clone() retains the
+    stylesheet copy made by XSLTProcessor::importStylesheet(). (iliaal)
+
 30 Jul 2026, PHP 8.6.0alpha3
 
 - Core:
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 000000000000..481925677d4e
--- /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 000000000000..72c47b73b002
--- /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 230c7bd39cc2..5d24ab1ec873 100644
--- a/ext/xsl/xsltprocessor.c
+++ b/ext/xsl/xsltprocessor.c
@@ -216,6 +216,12 @@ PHP_METHOD(XSLTProcessor, importStylesheet)
 	newdoc = nodep->doc;
 	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);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.