From c9220b1ae393800ca0e491bed4b5d0e1c2762afb Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 10 Nov 2026 07:11:21 -0500 Subject: [PATCH] ext/xsl: Fix use-after-free on a nested transform of the same processor php_xsl_apply_stylesheet() keeps the transformation's document reference in intern->doc and the callback node wrappers in the processor's node list, so a second transformation on the same processor, started from a php:function() callback or from main code while a Fiber is suspended inside one, freed that state under the running transformation. Throw an Error for such a call, as importStylesheet() already does. --- NEWS | 4 + UPGRADING | 7 ++ .../tests/xsltprocessor_nested_transform.phpt | 100 ++++++++++++++++++ ext/xsl/xsltprocessor.c | 7 ++ 4 files changed, 118 insertions(+) create mode 100644 ext/xsl/tests/xsltprocessor_nested_transform.phpt 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) {