Commit b55c619356c for php.net

commit b55c619356c810144a68596f5b15c7d9f1354b73
Author: David Carlier <devnexen@gmail.com>
Date:   Thu Sep 17 20:03:18 2026 +0100

    ext/xsl: XSLTProcessor::importStylesheet() use-after-free during a transformation.

    Fix #23730

    Importing a stylesheet from a php:function callback freed the stylesheet
    libxslt was still applying, and the transformation methods kept using the
    stale pointer to save the result. The transformation depth is now tracked
    on the object and importStylesheet() throws while it is non-zero.

    Close GH-23737

diff --git a/NEWS b/NEWS
index 42534ef4c37..c38d108d840 100644
--- a/NEWS
+++ b/NEWS
@@ -40,6 +40,10 @@ PHP                                                                        NEWS
   . Fixed a crash when SQLite3::close() is called from a userland callback.
     (Ilia Alshanetsky)

+- XSL:
+  . Fixed bug GH-23730 (use-after-free when XSLTProcessor::importStylesheet()
+    is called during a transformation). (David Carlier)
+
 - Zip:
   . Fixed ZipArchive::extractTo() ignoring files given in a non-list array.
     (David Carlier)
diff --git a/ext/xsl/php_xsl.h b/ext/xsl/php_xsl.h
index 36bd9cc7284..ac4ac5fc2fb 100644
--- a/ext/xsl/php_xsl.h
+++ b/ext/xsl/php_xsl.h
@@ -55,6 +55,7 @@ extern zend_module_entry xsl_module_entry;
 typedef struct xsl_object {
 	void *ptr;
 	HashTable *parameter;
+	uint32_t transform_depth;
 	bool hasKeys;
 	php_libxml_ref_obj *sheet_ref_obj;
 	zend_long securityPrefs;
diff --git a/ext/xsl/tests/gh23730.phpt b/ext/xsl/tests/gh23730.phpt
new file mode 100644
index 00000000000..893f6277734
--- /dev/null
+++ b/ext/xsl/tests/gh23730.phpt
@@ -0,0 +1,81 @@
+--TEST--
+GH-23730 (Use-after-free when a stylesheet is imported during a transformation)
+--EXTENSIONS--
+dom
+xsl
+--CREDITS--
+djarfluka
+--FILE--
+<?php
+
+class MyElement extends DOMElement {
+    public function __destruct() {
+        /* Runs while the node list of the finished transformation is torn down. */
+        import_other('destructor');
+    }
+}
+
+function import_other(string $from) {
+    try {
+        $GLOBALS['proc']->importStylesheet($GLOBALS['other']);
+        echo $from, ': no error', PHP_EOL;
+    } catch (Error $e) {
+        echo $from, ': ', $e::class, ': ', $e->getMessage(), PHP_EOL;
+    }
+}
+
+function callback($nodes) {
+    import_other('callback');
+    return $nodes[0];
+}
+
+$xml = new DOMDocument();
+$xml->registerNodeClass(DOMElement::class, MyElement::class);
+$xml->loadXML('<root><item>a</item></root>');
+
+$xsl = new DOMDocument();
+$xsl->loadXML(<<<XML
+<?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="/"><xsl:value-of select="php:function('callback', //item)"/></xsl:template>
+</xsl:stylesheet>
+XML);
+
+$other = new DOMDocument();
+$other->loadXML('<?xml version="1.0"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:template match="/">REPLACED</xsl:template></xsl:stylesheet>');
+
+$proc = new XSLTProcessor();
+$proc->registerPHPFunctions();
+$proc->importStylesheet($xsl);
+
+$GLOBALS['proc'] = $proc;
+$GLOBALS['other'] = $other;
+
+var_dump($proc->transformToXml($xml));
+var_dump($proc->transformToDoc($xml)->textContent);
+
+$uri = tempnam(sys_get_temp_dir(), 'gh23730');
+var_dump($proc->transformToUri($xml, $uri) > 0);
+@unlink($uri);
+
+/* Importing outside of a transformation is still allowed. */
+var_dump($proc->importStylesheet($other));
+var_dump($proc->transformToXml($xml));
+
+?>
+--EXPECT--
+callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress
+destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress
+string(24) "<?xml version="1.0"?>
+a
+"
+callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress
+destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress
+string(1) "a"
+callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress
+destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress
+bool(true)
+bool(true)
+string(31) "<?xml version="1.0"?>
+REPLACED
+"
diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c
index cf5a941d95c..fa6b2d41e60 100644
--- a/ext/xsl/xsltprocessor.c
+++ b/ext/xsl/xsltprocessor.c
@@ -175,6 +175,12 @@ PHP_METHOD(XSLTProcessor, importStylesheet)
 		RETURN_THROWS();
 	}

+	xsl_object *intern = Z_XSL_P(id);
+	if (UNEXPECTED(intern->transform_depth > 0)) {
+		zend_throw_error(NULL, "Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress");
+		RETURN_THROWS();
+	}
+
 	nodep = php_libxml_import_node(docp);
 	if (nodep == NULL) {
 		zend_argument_type_error(1, "must be a valid XML node");
@@ -251,8 +257,6 @@ PHP_METHOD(XSLTProcessor, importStylesheet)
 		RETURN_FALSE;
 	}

-	xsl_object *intern = Z_XSL_P(id);
-
 	/* Detach object */
 	clone_lxml_obj->document->ptr = NULL;
 	/* The namespace mappings need to be kept alive.
@@ -333,6 +337,8 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl
 		return NULL;
 	}

+	intern->transform_depth++;
+
 	if (intern->profiling) {
 		if (php_check_open_basedir(ZSTR_VAL(intern->profiling))) {
 			f = NULL;
@@ -438,6 +444,8 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl
 	efree(intern->doc);
 	intern->doc = NULL;

+	intern->transform_depth--;
+
 	return newdocp;

 }