Commit 26ab037d7c2 for php.net
commit 26ab037d7c2a07115801756ebec2ed8c0536ad4c
Author: David Carlier <devnexen@gmail.com>
Date: Tue Mar 24 12:23:25 2026 +0000
Fix GH-21496: UAF in dom_objects_free_storage.
Cloning a non-document DOM node creates a copy within the same
xmlDoc. importStylesheet then passes that original document to
xsltParseStylesheetDoc, which may strip and free nodes during
processing, invalidating PHP objects still referencing them.
Resolve the ownerDocument for non-document nodes and clone that
instead.
close GH-21500
diff --git a/NEWS b/NEWS
index d31184822e6..b7cc2595711 100644
--- a/NEWS
+++ b/NEWS
@@ -68,6 +68,8 @@ PHP NEWS
- XSL:
. Fix GH-21357 (XSLTProcessor works with DOMDocument, but fails with
Dom\XMLDocument). (ndossche)
+ . Fixed bug GH-21496 (UAF in dom_objects_free_storage).
+ (David Carlier/ndossche)
12 Mar 2026, PHP 8.4.19
diff --git a/ext/xsl/tests/gh21496.phpt b/ext/xsl/tests/gh21496.phpt
new file mode 100644
index 00000000000..7a5cea37937
--- /dev/null
+++ b/ext/xsl/tests/gh21496.phpt
@@ -0,0 +1,32 @@
+--TEST--
+GH-21496 (UAF in dom_objects_free_storage when importing non-document node as stylesheet)
+--EXTENSIONS--
+dom
+xsl
+--CREDITS--
+YuanchengJiang
+--FILE--
+<?php
+$comment = new DOMComment("my value");
+$doc = new DOMDocument();
+$doc->loadXML(<<<XML
+<container/>
+XML);
+$doc->documentElement->appendChild($comment);
+unset($doc);
+$proc = new XSLTProcessor();
+var_dump($proc->importStylesheet($comment));
+$sxe = simplexml_load_string('<container/>');
+$proc = new XSLTProcessor();
+$proc->importStylesheet($sxe);
+?>
+--EXPECTF--
+Warning: XSLTProcessor::importStylesheet(): compilation error: file %s line 1 element container in %s on line %d
+
+Warning: XSLTProcessor::importStylesheet(): xsltParseStylesheetProcess : document is not a stylesheet in %s on line %d
+bool(false)
+
+Warning: XSLTProcessor::importStylesheet(): compilation error: element container in %s on line %d
+
+Warning: XSLTProcessor::importStylesheet(): xsltParseStylesheetProcess : document is not a stylesheet in %s on line %d
+
diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c
index d066e270651..71971332a25 100644
--- a/ext/xsl/xsltprocessor.c
+++ b/ext/xsl/xsltprocessor.c
@@ -167,7 +167,7 @@ PHP_METHOD(XSLTProcessor, importStylesheet)
xsltStylesheetPtr sheetp;
bool clone_docu = false;
xmlNode *nodep = NULL;
- zval *cloneDocu, rv, clone_zv;
+ zval *cloneDocu, rv, clone_zv, owner_zv;
zend_string *member;
id = ZEND_THIS;
@@ -175,10 +175,40 @@ PHP_METHOD(XSLTProcessor, importStylesheet)
RETURN_THROWS();
}
+ nodep = php_libxml_import_node(docp);
+ if (nodep == NULL) {
+ zend_argument_type_error(1, "must be a valid XML node");
+ RETURN_THROWS();
+ }
+
+ if (Z_OBJ_HANDLER_P(docp, clone_obj) == NULL) {
+ zend_argument_type_error(1, "must be a cloneable node");
+ RETURN_THROWS();
+ }
+
+ ZVAL_UNDEF(&owner_zv);
+
+ /* For non-document nodes, resolve the ownerDocument and clone that
+ * instead as xsltParseStylesheetProcess may free nodes in the document. */
+ if (nodep->type != XML_DOCUMENT_NODE && nodep->type != XML_HTML_DOCUMENT_NODE) {
+ if (nodep->doc == NULL) {
+ zend_argument_value_error(1, "must be part of a document");
+ RETURN_THROWS();
+ }
+
+ /* See dom_import_simplexml_common */
+
+ dom_object *nodeobj = (dom_object *) ((char *) Z_OBJ_P(docp) - Z_OBJ_HT_P(docp)->offset);
+
+ php_dom_create_object((xmlNodePtr) nodep->doc, &owner_zv, nodeobj);
+ docp = &owner_zv;
+ }
+
/* libxslt uses _private, so we must copy the imported
* stylesheet document otherwise the node proxies will be a mess.
* We will clone the object and detach the libxml internals later. */
zend_object *clone = Z_OBJ_HANDLER_P(docp, clone_obj)(Z_OBJ_P(docp));
+ zval_ptr_dtor(&owner_zv);
if (!clone) {
RETURN_THROWS();
}