Skip to content

Commit bb092ab

Browse files
committed
Fix #80927: Removing documentElement after creating attribute node: possible use-after-free
Closes GH-11892.
1 parent f907a00 commit bb092ab

File tree

8 files changed

+163
-33
lines changed

8 files changed

+163
-33
lines changed

NEWS

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ PHP NEWS
88
- DOM:
99
. adoptNode now respects the strict error checking property. (nielsdos)
1010
. Align DOMChildNode parent checks with spec. (nielsdos)
11+
. Fixed bug #80927 (Removing documentElement after creating attribute node:
12+
possible use-after-free). (nielsdos)
1113

1214
- Opcache:
1315
. Avoid resetting JIT counter handlers from multiple processes/threads.

UPGRADING.INTERNALS

+2
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ PHP 8.3 INTERNALS UPGRADE NOTES
148148
dom_parent_node_before() now use an uint32_t argument for the number of nodes instead of int.
149149
- There is now a helper function php_dom_get_content_into_zval() to get the contents of a node.
150150
This avoids allocation if possible.
151+
- The function dom_set_old_ns() has been moved into ext/libxml as php_libxml_set_old_ns() and
152+
is now publicly exposed as an API.
151153

152154
g. ext/libxml
153155
- Two new functions: php_libxml_invalidate_node_list_cache_from_doc() and

ext/dom/element.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -1497,7 +1497,7 @@ PHP_METHOD(DOMElement, toggleAttribute)
14971497
}
14981498

14991499
ns->next = NULL;
1500-
dom_set_old_ns(thisp->doc, ns);
1500+
php_libxml_set_old_ns(thisp->doc, ns);
15011501
dom_reconcile_ns(thisp->doc, thisp);
15021502
} else {
15031503
/* TODO: in the future when namespace bugs are fixed,

ext/dom/php_dom.c

+1-30
Original file line numberDiff line numberDiff line change
@@ -1436,35 +1436,6 @@ void dom_normalize (xmlNodePtr nodep)
14361436
}
14371437
/* }}} end dom_normalize */
14381438

1439-
1440-
/* {{{ void dom_set_old_ns(xmlDoc *doc, xmlNs *ns) */
1441-
void dom_set_old_ns(xmlDoc *doc, xmlNs *ns) {
1442-
if (doc == NULL)
1443-
return;
1444-
1445-
ZEND_ASSERT(ns->next == NULL);
1446-
1447-
/* Note: we'll use a prepend strategy instead of append to
1448-
* make sure we don't lose performance when the list is long.
1449-
* As libxml2 could assume the xml node is the first one, we'll place our
1450-
* new entries after the first one. */
1451-
1452-
if (doc->oldNs == NULL) {
1453-
doc->oldNs = (xmlNsPtr) xmlMalloc(sizeof(xmlNs));
1454-
if (doc->oldNs == NULL) {
1455-
return;
1456-
}
1457-
memset(doc->oldNs, 0, sizeof(xmlNs));
1458-
doc->oldNs->type = XML_LOCAL_NAMESPACE;
1459-
doc->oldNs->href = xmlStrdup(XML_XML_NAMESPACE);
1460-
doc->oldNs->prefix = xmlStrdup((const xmlChar *)"xml");
1461-
} else {
1462-
ns->next = doc->oldNs->next;
1463-
}
1464-
doc->oldNs->next = ns;
1465-
}
1466-
/* }}} end dom_set_old_ns */
1467-
14681439
static void dom_reconcile_ns_internal(xmlDocPtr doc, xmlNodePtr nodep, xmlNodePtr search_parent)
14691440
{
14701441
xmlNsPtr nsptr, nsdftptr, curns, prevns = NULL;
@@ -1486,7 +1457,7 @@ static void dom_reconcile_ns_internal(xmlDocPtr doc, xmlNodePtr nodep, xmlNodePt
14861457
/* Note: we can't get here if the ns is already on the oldNs list.
14871458
* This is because in that case the definition won't be on the node, and
14881459
* therefore won't be in the nodep->nsDef list. */
1489-
dom_set_old_ns(doc, curns);
1460+
php_libxml_set_old_ns(doc, curns);
14901461
curns = prevns;
14911462
}
14921463
}

ext/dom/php_dom.h

-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ void php_dom_throw_error_with_message(int error_code, char *error_message, int s
128128
void node_list_unlink(xmlNodePtr node);
129129
int dom_check_qname(char *qname, char **localname, char **prefix, int uri_len, int name_len);
130130
xmlNsPtr dom_get_ns(xmlNodePtr node, char *uri, int *errorcode, char *prefix);
131-
void dom_set_old_ns(xmlDoc *doc, xmlNs *ns);
132131
void dom_reconcile_ns(xmlDocPtr doc, xmlNodePtr nodep);
133132
void dom_reconcile_ns_list(xmlDocPtr doc, xmlNodePtr nodep, xmlNodePtr last);
134133
xmlNsPtr dom_get_nsdecl(xmlNode *node, xmlChar *localName);

ext/dom/tests/bug80927.phpt

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
--TEST--
2+
Bug #80927 (Removing documentElement after creating attribute node: possible use-after-free)
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
function test1() {
9+
$dom = new DOMDocument();
10+
$dom->appendChild($dom->createElement("html"));
11+
$a = $dom->createAttributeNS("fake_ns", "test:test");
12+
$dom->removeChild($dom->documentElement);
13+
14+
echo $dom->saveXML();
15+
16+
var_dump($a->namespaceURI);
17+
var_dump($a->prefix);
18+
}
19+
20+
enum Test2Variation {
21+
case REMOVE_DOCUMENT;
22+
case REMOVE_CHILD;
23+
}
24+
25+
function test2(Test2Variation $variation) {
26+
$dom = new DOMDocument();
27+
$dom->appendChild($dom->createElement("html"));
28+
$a = $dom->createAttributeNS("fake_ns", "test:test");
29+
30+
$foo = $dom->appendChild($dom->createElement('foo'));
31+
$foo->appendChild($dom->documentElement);
32+
33+
unset($foo);
34+
35+
match ($variation) {
36+
Test2Variation::REMOVE_DOCUMENT => $dom->documentElement->remove(),
37+
Test2Variation::REMOVE_CHILD => $dom->documentElement->firstElementChild->remove(),
38+
};
39+
40+
echo $dom->saveXML();
41+
42+
var_dump($a->namespaceURI);
43+
var_dump($a->prefix);
44+
}
45+
46+
function test3() {
47+
$dom = new DOMDocument();
48+
$dom->appendChild($dom->createElement('html'));
49+
$foobar = $dom->documentElement->appendChild($dom->createElementNS('some:ns', 'foo:bar'));
50+
$foobar2 = $foobar->appendChild($dom->createElementNS('some:ns', 'foo:bar2'));
51+
$foobar->remove();
52+
unset($foobar);
53+
$dom->documentElement->appendChild($foobar2);
54+
55+
echo $dom->saveXML();
56+
57+
var_dump($foobar2->namespaceURI);
58+
var_dump($foobar2->prefix);
59+
}
60+
61+
echo "--- Remove namespace declarator for attribute, root ---\n";
62+
test1();
63+
echo "--- Remove namespace declarator for attribute, moved root ---\n";
64+
test2(Test2Variation::REMOVE_DOCUMENT);
65+
echo "--- Remove namespace declarator for attribute, moved root child ---\n";
66+
test2(Test2Variation::REMOVE_CHILD);
67+
echo "--- Remove namespace declarator for element ---\n";
68+
test3();
69+
70+
?>
71+
--EXPECT--
72+
--- Remove namespace declarator for attribute, root ---
73+
<?xml version="1.0"?>
74+
string(7) "fake_ns"
75+
string(4) "test"
76+
--- Remove namespace declarator for attribute, moved root ---
77+
<?xml version="1.0"?>
78+
string(7) "fake_ns"
79+
string(4) "test"
80+
--- Remove namespace declarator for attribute, moved root child ---
81+
<?xml version="1.0"?>
82+
<foo/>
83+
string(7) "fake_ns"
84+
string(4) "test"
85+
--- Remove namespace declarator for element ---
86+
<?xml version="1.0"?>
87+
<html><foo:bar2 xmlns:foo="some:ns"/></html>
88+
string(7) "some:ns"
89+
string(3) "foo"

ext/libxml/libxml.c

+67-1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,39 @@ zend_module_entry libxml_module_entry = {
103103

104104
/* }}} */
105105

106+
static void php_libxml_set_old_ns_list(xmlDocPtr doc, xmlNsPtr first, xmlNsPtr last)
107+
{
108+
if (UNEXPECTED(doc == NULL)) {
109+
return;
110+
}
111+
112+
ZEND_ASSERT(last->next == NULL);
113+
114+
/* Note: we'll use a prepend strategy instead of append to
115+
* make sure we don't lose performance when the list is long.
116+
* As libxml2 could assume the xml node is the first one, we'll place our
117+
* new entries after the first one. */
118+
119+
if (UNEXPECTED(doc->oldNs == NULL)) {
120+
doc->oldNs = (xmlNsPtr) xmlMalloc(sizeof(xmlNs));
121+
if (doc->oldNs == NULL) {
122+
return;
123+
}
124+
memset(doc->oldNs, 0, sizeof(xmlNs));
125+
doc->oldNs->type = XML_LOCAL_NAMESPACE;
126+
doc->oldNs->href = xmlStrdup(XML_XML_NAMESPACE);
127+
doc->oldNs->prefix = xmlStrdup((const xmlChar *)"xml");
128+
} else {
129+
last->next = doc->oldNs->next;
130+
}
131+
doc->oldNs->next = first;
132+
}
133+
134+
PHP_LIBXML_API void php_libxml_set_old_ns(xmlDocPtr doc, xmlNsPtr ns)
135+
{
136+
php_libxml_set_old_ns_list(doc, ns, ns);
137+
}
138+
106139
static void php_libxml_unlink_entity(void *data, void *table, const xmlChar *name)
107140
{
108141
xmlEntityPtr entity = data;
@@ -211,8 +244,41 @@ static void php_libxml_node_free(xmlNodePtr node)
211244
xmlHashScan(dtd->pentities, php_libxml_unlink_entity, dtd->pentities);
212245
/* No unlinking of notations, see remark above at case XML_NOTATION_NODE. */
213246
}
214-
ZEND_FALLTHROUGH;
247+
xmlFreeNode(node);
248+
break;
215249
}
250+
case XML_ELEMENT_NODE:
251+
if (node->nsDef && node->doc) {
252+
/* Make the namespace declaration survive the destruction of the holding element.
253+
* This prevents a use-after-free on the namespace declaration.
254+
*
255+
* The main problem is that libxml2 doesn't have a reference count on the namespace declaration.
256+
* We don't actually need to save the namespace declaration if we know the subtree it belongs to
257+
* has no references from userland. However, we can't know that without traversing the whole subtree
258+
* (=> slow), or without adding some subtree metadata (=> also slow).
259+
* So we have to assume we need to save everything.
260+
*
261+
* However, namespace declarations are quite rare in comparison to other node types.
262+
* Most node types are either elements, text or attributes.
263+
* And you only need one namespace declaration per namespace (in principle).
264+
* So I expect the number of namespace declarations to be low for an average XML document.
265+
*
266+
* In the worst possible case we have to save all namespace declarations when we for example remove
267+
* the whole document. But given the above reasoning this likely won't be a lot of declarations even
268+
* in the worst case.
269+
* A single declaration only takes about 48 bytes of memory, and I don't expect the worst case to occur
270+
* very often (why would you remove the whole document?).
271+
*/
272+
xmlNsPtr ns = node->nsDef;
273+
xmlNsPtr last = ns;
274+
while (last->next) {
275+
last = last->next;
276+
}
277+
php_libxml_set_old_ns_list(node->doc, ns, last);
278+
node->nsDef = NULL;
279+
}
280+
xmlFreeNode(node);
281+
break;
216282
default:
217283
xmlFreeNode(node);
218284
break;

ext/libxml/php_libxml.h

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ PHP_LIBXML_API int php_libxml_xmlCheckUTF8(const unsigned char *s);
134134
PHP_LIBXML_API void php_libxml_switch_context(zval *context, zval *oldcontext);
135135
PHP_LIBXML_API void php_libxml_issue_error(int level, const char *msg);
136136
PHP_LIBXML_API bool php_libxml_disable_entity_loader(bool disable);
137+
PHP_LIBXML_API void php_libxml_set_old_ns(xmlDocPtr doc, xmlNsPtr ns);
137138

138139
/* Init/shutdown functions*/
139140
PHP_LIBXML_API void php_libxml_initialize(void);

0 commit comments

Comments
 (0)