Skip to content

Commit 003ebdd

Browse files
committed
Fix phpGH-9628: Implicitly removing nodes from \DOMDocument breaks existing references
Change the way lifetime works in ext/libxml and ext/dom Previously, a node could be freed even when holding a userland reference to it. This resulted in exceptions when trying to access that node after it has been implicitly or explicitly removed. After this patch, a node will only be freed when the last userland reference disappears. Fixes phpGH-9628. Closes phpGH-11576.
1 parent f62757e commit 003ebdd

25 files changed

+849
-31
lines changed

ext/dom/php_dom.c

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,6 @@ zval *dom_read_property(zend_object *object, zend_string *name, int type, void *
310310

311311
if (obj->prop_handler != NULL) {
312312
hnd = zend_hash_find_ptr(obj->prop_handler, name);
313-
} else if (instanceof_function(obj->std.ce, dom_node_class_entry)) {
314-
zend_throw_error(NULL, "Couldn't fetch %s. Node no longer exists", ZSTR_VAL(obj->std.ce->name));
315-
retval = &EG(uninitialized_zval);
316-
return retval;
317313
}
318314

319315
if (hnd) {

ext/dom/tests/DOMAttr_ownerElement_error_001.phpt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@ $document->appendChild($root);
1414
$attr = $root->setAttribute('category', 'books');
1515
$document->removeChild($root);
1616
$root = null;
17-
try {
18-
var_dump($attr->ownerElement);
19-
} catch (\Error $e) {
20-
echo get_class($e) . ': ' . $e->getMessage() . \PHP_EOL;
21-
}
17+
var_dump($attr->ownerElement);
2218
?>
2319
--EXPECT--
24-
Error: Couldn't fetch DOMAttr. Node no longer exists
20+
NULL

ext/dom/tests/bug36756.phpt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,11 @@ $node = $xpath->query('//child')->item(0);
2020
echo $node->nodeName . "\n";
2121
$GLOBALS['dom']->removeChild($GLOBALS['dom']->firstChild);
2222

23-
try {
24-
echo "nodeType: " . $node->nodeType . "\n";
25-
} catch (\Error $e) {
26-
echo get_class($e) . ': ' . $e->getMessage() .\PHP_EOL;
27-
}
23+
echo "nodeType: " . $node->nodeType . "\n";
2824

2925
?>
3026
--EXPECT--
3127
root
3228
nodeType: 1
3329
child
34-
Error: Couldn't fetch DOMElement. Node no longer exists
30+
nodeType: 1

ext/dom/tests/bug70359.phpt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,7 @@ var_dump($spaceNode->nodeType);
2929
var_dump($spaceNode->nodeValue);
3030

3131
$dom->documentElement->firstElementChild->remove();
32-
try {
33-
print_r($spaceNode->parentNode);
34-
} catch (\Error $e) {
35-
echo $e->getMessage(), "\n";
36-
}
32+
var_dump($spaceNode->parentNode);
3733

3834
echo "-- Test with parent and ns attribute --\n";
3935

@@ -67,7 +63,7 @@ DOMNameSpaceNode Object
6763
-- Test with parent and non-ns attribute --
6864
int(2)
6965
string(3) "bar"
70-
Couldn't fetch DOMAttr. Node no longer exists
66+
NULL
7167
-- Test with parent and ns attribute --
7268
DOMNameSpaceNode Object
7369
(
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Delayed freeing attribute declaration
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
$doc = new DOMDocument;
8+
$doc->loadXML(<<<'XML'
9+
<?xml version="1.0"?>
10+
<!DOCTYPE book [
11+
<!ELEMENT book (#PCDATA)>
12+
<!ATTLIST book
13+
title CDATA #REQUIRED
14+
>
15+
]>
16+
<book title="book title"/>
17+
XML);
18+
echo $doc->saveXML(), "\n";
19+
// Note: no way to query the attribute declaration, but this at least tests destruction order
20+
$doc->removeChild($doc->doctype);
21+
echo $doc->saveXML(), "\n";
22+
?>
23+
--EXPECT--
24+
<?xml version="1.0"?>
25+
<!DOCTYPE book [
26+
<!ELEMENT book (#PCDATA)>
27+
<!ATTLIST book title CDATA #REQUIRED>
28+
]>
29+
<book title="book title"/>
30+
31+
<?xml version="1.0"?>
32+
<book title="book title"/>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
--TEST--
2+
Delayed freeing comment node
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
$doc = new DOMDocument;
8+
$comment = $doc->appendChild($doc->createElement('container'))
9+
->appendChild($doc->createComment('my comment'));
10+
echo $doc->saveXML(), "\n";
11+
$comment->parentNode->remove();
12+
echo $doc->saveXML(), "\n";
13+
echo $doc->saveXML($comment), "\n";
14+
var_dump($comment->parentNode);
15+
?>
16+
--EXPECT--
17+
<?xml version="1.0"?>
18+
<container><!--my comment--></container>
19+
20+
<?xml version="1.0"?>
21+
22+
<!--my comment-->
23+
NULL
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
--TEST--
2+
Tests with direction construction
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
function node_alike_test($test) {
8+
try {
9+
var_dump($test->parentNode);
10+
var_dump($test->nodeValue);
11+
} catch (Throwable $e) {
12+
echo $e->getMessage(), "\n";
13+
}
14+
try {
15+
var_dump($test->appendChild($test));
16+
} catch (Throwable $e) {
17+
echo $e->getMessage(), "\n";
18+
}
19+
}
20+
21+
echo "-- Test DOMCharacterData --\n";
22+
$test = new DOMCharacterData("test");
23+
try {
24+
var_dump($test->textContent);
25+
} catch (Throwable $e) {
26+
echo $e->getMessage(), "\n";
27+
}
28+
try {
29+
var_dump($test->appendData('test'));
30+
} catch (Throwable $e) {
31+
echo $e->getMessage(), "\n";
32+
}
33+
34+
echo "-- Test DOMCdataSection --\n";
35+
$test = new DOMCdataSection("test");
36+
var_dump($test->textContent);
37+
var_dump($test->appendData('test'));
38+
39+
echo "-- Test DOMText --\n";
40+
$test = new DOMText("test");
41+
try {
42+
var_dump($test->wholeText);
43+
var_dump($test->parentNode);
44+
} catch (Throwable $e) {
45+
echo $e->getMessage(), "\n";
46+
}
47+
try {
48+
var_dump($test->isWhitespaceInElementContent());
49+
} catch (Throwable $e) {
50+
echo $e->getMessage(), "\n";
51+
}
52+
53+
echo "-- Test DOMComment --\n";
54+
$test = new DOMComment("my comment");
55+
var_dump($test->textContent);
56+
var_dump($test->parentNode);
57+
var_dump($test->getLineNo());
58+
59+
echo "-- Test DOMElement --\n";
60+
$test = new DOMElement("qualifiedName", "test");
61+
var_dump($test->textContent);
62+
var_dump($test->parentNode);
63+
try {
64+
var_dump($test->appendChild($test));
65+
} catch (Throwable $e) {
66+
echo $e->getMessage(), "\n";
67+
}
68+
69+
echo "-- Test DOMNode --\n";
70+
node_alike_test(new DOMNode());
71+
72+
echo "-- Test DOMNotation --\n";
73+
node_alike_test(new DOMNotation());
74+
75+
echo "-- Test DOMProcessingInstruction --\n";
76+
node_alike_test(new DOMProcessingInstruction("name", "value"));
77+
78+
echo "-- Test DOMEntity --\n";
79+
$test = new DOMEntity();
80+
try {
81+
var_dump($test->nodeValue);
82+
var_dump($test->parentNode);
83+
} catch (Throwable $e) {
84+
echo $e->getMessage(), "\n";
85+
}
86+
try {
87+
var_dump($test->appendChild($test));
88+
} catch (Throwable $e) {
89+
echo $e->getMessage(), "\n";
90+
}
91+
92+
echo "-- Test DOMAttr --\n";
93+
$test = new DOMAttr("attr", "value");
94+
var_dump($test->nodeValue);
95+
var_dump($test->parentNode);
96+
try {
97+
var_dump($test->appendChild($test));
98+
} catch (Throwable $e) {
99+
echo $e->getMessage(), "\n";
100+
}
101+
?>
102+
--EXPECT--
103+
-- Test DOMCharacterData --
104+
Invalid State Error
105+
Couldn't fetch DOMCharacterData
106+
-- Test DOMCdataSection --
107+
string(4) "test"
108+
bool(true)
109+
-- Test DOMText --
110+
string(4) "test"
111+
NULL
112+
bool(false)
113+
-- Test DOMComment --
114+
string(10) "my comment"
115+
NULL
116+
int(0)
117+
-- Test DOMElement --
118+
string(4) "test"
119+
NULL
120+
No Modification Allowed Error
121+
-- Test DOMNode --
122+
Invalid State Error
123+
Couldn't fetch DOMNode
124+
-- Test DOMNotation --
125+
Invalid State Error
126+
Couldn't fetch DOMNotation
127+
-- Test DOMProcessingInstruction --
128+
NULL
129+
string(5) "value"
130+
bool(false)
131+
-- Test DOMEntity --
132+
Invalid State Error
133+
Couldn't fetch DOMEntity
134+
-- Test DOMAttr --
135+
string(5) "value"
136+
NULL
137+
No Modification Allowed Error
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Delayed freeing document fragment
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
$doc = new DOMDocument;
8+
$frag = $doc->createDocumentFragment();
9+
$frag->appendChild($doc->createElementNS('some:ns', 'child', 'text content'));
10+
$child = $doc->appendChild($doc->createElement('root'))->appendChild($frag);
11+
var_dump($doc->textContent);
12+
$doc->documentElement->remove();
13+
var_dump($doc->textContent);
14+
unset($doc);
15+
var_dump($child->textContent);
16+
17+
$doc = new DOMDocument;
18+
$doc->appendChild($doc->createElement('container'));
19+
$doc->documentElement->appendChild($doc->importNode($frag));
20+
unset($frag);
21+
var_dump($doc->textContent);
22+
23+
var_dump($child->parentNode);
24+
?>
25+
--EXPECTF--
26+
string(12) "text content"
27+
string(0) ""
28+
string(12) "text content"
29+
30+
Warning: DOMNode::appendChild(): Document Fragment is empty in %s on line %d
31+
string(0) ""
32+
NULL
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
--TEST--
2+
Delayed freeing character data
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
$doc = new DOMDocument;
8+
$doc->loadXML(<<<'XML'
9+
<?xml version="1.0"?>
10+
<container><![CDATA[This is a CDATA section<p>test</p>]]></container>
11+
XML);
12+
$cdata = $doc->documentElement->firstChild;
13+
var_dump($cdata->wholeText, $cdata->parentNode->tagName);
14+
$cdata->parentNode->remove();
15+
var_dump($cdata->wholeText, $cdata->parentNode->tagName);
16+
?>
17+
--EXPECTF--
18+
string(34) "This is a CDATA section<p>test</p>"
19+
string(9) "container"
20+
21+
Warning: Attempt to read property "tagName" on null in %s on line %d
22+
string(34) "This is a CDATA section<p>test</p>"
23+
NULL
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
Delayed freeing dtd node
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
$doc = new DOMDocument;
8+
$dtd = $doc->implementation->createDocumentType('qualified name', 'public', 'system');
9+
$doc = $doc->implementation->createDocument('', '', $dtd);
10+
echo $doc->saveXML(), "\n";
11+
unset($doc);
12+
echo $dtd->ownerDocument->saveXML();
13+
$dtd->ownerDocument->removeChild($dtd);
14+
var_dump($dtd->ownerDocument->nodeName);
15+
?>
16+
--EXPECT--
17+
<?xml version="1.0"?>
18+
<!DOCTYPE qualified name PUBLIC "public" "system">
19+
20+
<?xml version="1.0"?>
21+
<!DOCTYPE qualified name PUBLIC "public" "system">
22+
string(9) "#document"

0 commit comments

Comments
 (0)