diff --git a/NEWS b/NEWS
index 5a865955281d..f32d6714a140 100644
--- a/NEWS
+++ b/NEWS
@@ -204,6 +204,10 @@ PHP NEWS
. Fixed xml_get_current_byte_index() returning negative values for byte
indexes beyond 2 GiB on Windows x64. (Marc Bennewitz)
+- XSL:
+ . Fixed a use-after-free when a transformation was started on a processor
+ that is already transforming. (Ilia Alshanetsky)
+
- Zlib:
. Fixed inflate_init() dropping the preset dictionary for raw streams with
a non-default window. (Ilia Alshanetsky)
diff --git a/UPGRADING b/UPGRADING
index a4df23352639..e0554f2014a7 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -64,6 +64,13 @@ PHP 8.7 UPGRADE NOTES
to 16 by default. Set the stream context option max_filter_count to change
this.
+- XSL:
+ . XSLTProcessor::transformToDoc(), XSLTProcessor::transformToUri() and
+ XSLTProcessor::transformToXml() now throw an Error when called while the
+ same processor is already transforming, for example from a php:function()
+ callback or from a suspended Fiber. Use a separate XSLTProcessor instance
+ instead.
+
========================================
2. New Features
========================================
diff --git a/ext/xsl/tests/xsltprocessor_nested_transform.phpt b/ext/xsl/tests/xsltprocessor_nested_transform.phpt
new file mode 100644
index 000000000000..cef56cc67925
--- /dev/null
+++ b/ext/xsl/tests/xsltprocessor_nested_transform.phpt
@@ -0,0 +1,100 @@
+--TEST--
+XSLTProcessor refuses a nested transformation on the same processor
+--EXTENSIONS--
+dom
+xsl
+--FILE--
+transformToXml($doc));
+ } catch (Error $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+ }
+ try {
+ var_dump($proc->transformToDoc($doc));
+ } catch (Error $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+ }
+ try {
+ var_dump($proc->transformToUri($doc, $GLOBALS['uri']));
+ } catch (Error $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+ }
+ return 'v';
+}
+
+function suspend($nodes) {
+ Fiber::suspend();
+ return 'f';
+}
+
+function text($nodes) {
+ return $nodes[0]->textContent;
+}
+
+$doc = new DOMDocument();
+$doc->loadXML('123');
+
+$xsl = new DOMDocument();
+$xsl->loadXML(<<
+
+
+ []
+
+XML);
+
+$uri = __DIR__ . '/xsltprocessor_nested_transform.xml';
+
+$proc = new XSLTProcessor();
+$proc->registerPHPFunctions();
+$proc->importStylesheet($xsl);
+
+var_dump($proc->transformToXml($doc));
+
+$proc->setParameter('', 'cb', 'suspend');
+$fiber = new Fiber(fn () => $proc->transformToXml($doc));
+$fiber->start();
+try {
+ var_dump($proc->transformToXml($doc));
+} catch (Error $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+while (!$fiber->isTerminated()) {
+ $fiber->resume();
+}
+var_dump($fiber->getReturn());
+
+$proc->setParameter('', 'cb', 'text');
+var_dump($proc->transformToXml($doc));
+var_dump($proc->transformToDoc($doc)->textContent);
+
+?>
+--CLEAN--
+
+--EXPECT--
+Error: Cannot call XSLTProcessor::transformToXml() while a transformation is in progress
+Error: Cannot call XSLTProcessor::transformToDoc() while a transformation is in progress
+Error: Cannot call XSLTProcessor::transformToUri() while a transformation is in progress
+string(35) "
+[va][vb][vc]
+"
+Error: Cannot call XSLTProcessor::transformToXml() while a transformation is in progress
+string(35) "
+[fa][fb][fc]
+"
+string(35) "
+[1a][2b][3c]
+"
+string(12) "[1a][2b][3c]"
diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c
index 3f6a7b6aa923..4737bf5643f7 100644
--- a/ext/xsl/xsltprocessor.c
+++ b/ext/xsl/xsltprocessor.c
@@ -323,6 +323,13 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl
return NULL;
}
+ if (UNEXPECTED(intern->transform_depth > 0)) {
+ zend_string *name = get_active_function_or_method_name();
+ zend_throw_error(NULL, "Cannot call %s() while a transformation is in progress", ZSTR_VAL(name));
+ zend_string_release(name);
+ return NULL;
+ }
+
intern->transform_depth++;
if (intern->profiling) {