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.

Closes GH-14704.
This commit is contained in:
Niels Dossche 2024-06-28 17:51:31 +02:00
parent 056bec72f4
commit 42908f9f68
No known key found for this signature in database
GPG Key ID: B8A8AD166DF0E2E5
3 changed files with 123 additions and 0 deletions

3
NEWS
View File

@ -9,6 +9,9 @@ PHP NEWS
. Fixed bug GH-14590 (Memory leak in FPM test gh13563-conf-bool-env.phpt.
(nielsdos)
- Dom:
. Fixed bug GH-14702 (DOMDocument::xinclude() crash). (nielsdos)
- Phar:
. Fixed bug GH-14603 (null string from zip entry).
(David Carlier)

View File

@ -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);

View File

@ -0,0 +1,66 @@
--TEST--
GH-14702 (DOMDocument::xinclude() crash)
--EXTENSIONS--
dom
--FILE--
<?php
$doc = new DOMDocument();
$doc->loadXML(<<<XML
<?xml version="1.0"?>
<root>
<child/>
<include href="foo" xmlns="http://www.w3.org/2001/XInclude">
<fallback/>
</include>
<keep/>
</root>
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)
<?xml version="1.0"?>
<root>
<fallback-child1/><fallback-child2/>
<keep/>
</root>
object(DOMElement)#4 (1) {
["schemaTypeInfo"]=>
NULL
}
object(DOMElement)#5 (1) {
["schemaTypeInfo"]=>
NULL
}
object(DOMElement)#3 (1) {
["schemaTypeInfo"]=>
NULL
}
string(4) "keep"
<?xml version="1.0"?>
<root>
<fallback-child1/><fallback-child2/>
<keep>still works</keep>
</root>