From ca9d7740b643f4d8d46e02e57bf8cece46b1a188 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Fri, 28 Jun 2024 17:51:31 +0200 Subject: [PATCH] Fix GH-14702: DOMDocument::xinclude() crash The xinclude code from libxml removes the fallback node, but the fallback node is still reference via $fallback. The solution is to detach the nodes that are going to be removed in advance. --- ext/dom/document.c | 54 +++++++++++++++++++++++++++++++ ext/dom/tests/gh14702.phpt | 66 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 ext/dom/tests/gh14702.phpt diff --git a/ext/dom/document.c b/ext/dom/document.c index a2772479cfe8e..38af4ca27c820 100644 --- a/ext/dom/document.c +++ b/ext/dom/document.c @@ -1566,6 +1566,58 @@ static void php_dom_remove_xinclude_nodes(xmlNodePtr cur) /* {{{ */ } /* }}} */ +/* Backported from master branch xml_common.h */ +static zend_always_inline xmlNodePtr php_dom_next_in_tree_order(const xmlNode *nodep, const xmlNode *basep) +{ + if (nodep->type == XML_ELEMENT_NODE && nodep->children) { + return nodep->children; + } + + if (nodep->next) { + return nodep->next; + } else { + /* Go upwards, until we find a parent node with a next sibling, or until we hit the base. */ + do { + nodep = nodep->parent; + if (nodep == basep) { + return NULL; + } + } while (nodep->next == NULL); + return nodep->next; + } +} + +static void dom_xinclude_strip_references(xmlNodePtr basep) +{ + php_libxml_node_free_resource(basep); + + xmlNodePtr current = basep->children; + + while (current) { + php_libxml_node_free_resource(current); + current = php_dom_next_in_tree_order(current, basep); + } +} + +/* See GH-14702. + * We have to remove userland references to xinclude fallback nodes because libxml2 will make clones of these + * and remove the original nodes. If the originals are removed while there are still userland references + * this will cause memory corruption. */ +static void dom_xinclude_strip_fallback_references(const xmlNode *basep) +{ + xmlNodePtr current = basep->children; + + while (current) { + if (current->type == XML_ELEMENT_NODE && current->ns != NULL && current->_private != NULL + && xmlStrEqual(current->name, XINCLUDE_FALLBACK) + && (xmlStrEqual(current->ns->href, XINCLUDE_NS) || xmlStrEqual(current->ns->href, XINCLUDE_OLD_NS))) { + dom_xinclude_strip_references(current); + } + + current = php_dom_next_in_tree_order(current, basep); + } +} + /* {{{ Substitutues xincludes in a DomDocument */ PHP_METHOD(DOMDocument, xinclude) { @@ -1588,6 +1640,8 @@ PHP_METHOD(DOMDocument, xinclude) DOM_GET_OBJ(docp, id, xmlDocPtr, intern); + dom_xinclude_strip_fallback_references((const xmlNode *) docp); + PHP_LIBXML_SANITIZE_GLOBALS(xinclude); err = xmlXIncludeProcessFlags(docp, (int)flags); PHP_LIBXML_RESTORE_GLOBALS(xinclude); diff --git a/ext/dom/tests/gh14702.phpt b/ext/dom/tests/gh14702.phpt new file mode 100644 index 0000000000000..9811bb7883acb --- /dev/null +++ b/ext/dom/tests/gh14702.phpt @@ -0,0 +1,66 @@ +--TEST-- +GH-14702 (DOMDocument::xinclude() crash) +--EXTENSIONS-- +dom +--FILE-- +loadXML(<< + + + + + + + +XML); +$xi = $doc->createElementNS('http://www.w3.org/2001/XInclude', 'xi:include'); +$xi->setAttribute('href', 'nonexistent'); + +$fallback = $doc->createElementNS('http://www.w3.org/2001/XInclude', 'xi:fallback'); +$xi->appendChild($fallback); +$child1 = $fallback->appendChild($doc->createElement('fallback-child1')); +$child2 = $fallback->appendChild($doc->createElement('fallback-child2')); + +$xpath = new DOMXPath($doc); +$toReplace = $xpath->query('//child')->item(0); +$toReplace->parentNode->replaceChild($xi, $toReplace); + +$keep = $doc->documentElement->lastElementChild; + +var_dump(@$doc->xinclude()); +echo $doc->saveXML(); + +var_dump($child1, $child2, $fallback, $keep->nodeName); + +$keep->textContent = 'still works'; +echo $doc->saveXML(); +?> +--EXPECT-- +int(2) + + + + + + +object(DOMElement)#4 (1) { + ["schemaTypeInfo"]=> + NULL +} +object(DOMElement)#5 (1) { + ["schemaTypeInfo"]=> + NULL +} +object(DOMElement)#3 (1) { + ["schemaTypeInfo"]=> + NULL +} +string(4) "keep" + + + + + still works +