Skip to content

Commit eebc528

Browse files
committed
Fix broken cache invalidation with deallocated and reallocated document node
The original caching implementation had an oversight in combination with the new lifetime management in DOM for 8.3. The modification counter is stored on the document object itself, but as that can get deallocated when all references disappear, stale cache data can be used. Normally this isn't a problem, unless getElementsByTagName is called not on the document but on a child node. Fix it by moving caching data into the ref object, which will outlive all nodes from a document even if the document object disappears. Closes phpGH-12338.
1 parent 31a44c8 commit eebc528

8 files changed

+86
-46
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ PHP NEWS
88

99
- DOM:
1010
. Restore old namespace reconciliation behaviour. (nielsdos)
11+
. Fix broken cache invalidation with deallocated and reallocated document
12+
node. (nielsdos)
1113

1214
- Fileinfo:
1315
. Fixed bug GH-11891 (fileinfo returns text/xml for some svg files). (usarise)

ext/dom/document.c

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,7 @@ PHP_METHOD(DOMDocument, importNode)
818818
}
819819
}
820820

821-
php_libxml_invalidate_node_list_cache_from_doc(docp);
821+
php_libxml_invalidate_node_list_cache(intern->document);
822822

823823
DOM_RET_OBJ((xmlNodePtr) retnodep, &ret, intern);
824824
}
@@ -1031,7 +1031,7 @@ bool php_dom_adopt_node(xmlNodePtr nodep, dom_object *dom_object_new_document, x
10311031
{
10321032
php_libxml_invalidate_node_list_cache_from_doc(nodep->doc);
10331033
if (nodep->doc != new_document) {
1034-
php_libxml_invalidate_node_list_cache_from_doc(new_document);
1034+
php_libxml_invalidate_node_list_cache(dom_object_new_document->document);
10351035

10361036
/* Note for ATTRIBUTE_NODE: specified is always true in ext/dom,
10371037
* and since this unlink it; the owner element will be unset (i.e. parentNode). */
@@ -1101,7 +1101,7 @@ PHP_METHOD(DOMDocument, normalizeDocument)
11011101

11021102
DOM_GET_OBJ(docp, id, xmlDocPtr, intern);
11031103

1104-
php_libxml_invalidate_node_list_cache_from_doc(docp);
1104+
php_libxml_invalidate_node_list_cache(intern->document);
11051105

11061106
dom_normalize((xmlNodePtr) docp);
11071107
}
@@ -1327,7 +1327,7 @@ static void dom_finish_loading_document(zval *this, zval *return_value, xmlDocPt
13271327
xmlDocPtr docp = (xmlDocPtr) dom_object_get_node(intern);
13281328
dom_doc_propsptr doc_prop = NULL;
13291329
if (docp != NULL) {
1330-
const php_libxml_doc_ptr *doc_ptr = docp->_private;
1330+
const php_libxml_ref_obj *doc_ptr = intern->document;
13311331
ZEND_ASSERT(doc_ptr != NULL); /* Must exist, we have a document */
13321332
old_modification_nr = doc_ptr->cache_tag.modification_nr;
13331333
php_libxml_decrement_node_ptr((php_libxml_node_object *) intern);
@@ -1348,9 +1348,8 @@ static void dom_finish_loading_document(zval *this, zval *return_value, xmlDocPt
13481348
php_libxml_increment_node_ptr((php_libxml_node_object *)intern, (xmlNodePtr)newdoc, (void *)intern);
13491349
/* Since iterators should invalidate, we need to start the modification number from the old counter */
13501350
if (old_modification_nr != 0) {
1351-
php_libxml_doc_ptr* doc_ptr = (php_libxml_doc_ptr*) ((php_libxml_node_object*) intern)->node; /* downcast */
1352-
doc_ptr->cache_tag.modification_nr = old_modification_nr;
1353-
php_libxml_invalidate_node_list_cache(doc_ptr);
1351+
intern->document->cache_tag.modification_nr = old_modification_nr;
1352+
php_libxml_invalidate_node_list_cache(intern->document);
13541353
}
13551354

13561355
RETURN_TRUE;
@@ -1611,7 +1610,7 @@ PHP_METHOD(DOMDocument, xinclude)
16111610
php_dom_remove_xinclude_nodes(root);
16121611
}
16131612

1614-
php_libxml_invalidate_node_list_cache_from_doc(docp);
1613+
php_libxml_invalidate_node_list_cache(intern->document);
16151614

16161615
if (err) {
16171616
RETVAL_LONG(err);

ext/dom/node.c

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ int dom_node_node_value_write(dom_object *obj, zval *newval)
201201
break;
202202
}
203203

204-
php_libxml_invalidate_node_list_cache_from_doc(nodep->doc);
204+
php_libxml_invalidate_node_list_cache(obj->document);
205205

206206
zend_string_release_ex(str, 0);
207207
return SUCCESS;
@@ -794,7 +794,7 @@ int dom_node_text_content_write(dom_object *obj, zval *newval)
794794
return FAILURE;
795795
}
796796

797-
php_libxml_invalidate_node_list_cache_from_doc(nodep->doc);
797+
php_libxml_invalidate_node_list_cache(obj->document);
798798

799799
/* Typed property, this is already a string */
800800
ZEND_ASSERT(Z_TYPE_P(newval) == IS_STRING);
@@ -919,7 +919,7 @@ PHP_METHOD(DOMNode, insertBefore)
919919
php_libxml_increment_doc_ref((php_libxml_node_object *)childobj, NULL);
920920
}
921921

922-
php_libxml_invalidate_node_list_cache_from_doc(parentp->doc);
922+
php_libxml_invalidate_node_list_cache(intern->document);
923923

924924
if (ref != NULL) {
925925
DOM_GET_OBJ(refp, ref, xmlNodePtr, refpobj);
@@ -1124,7 +1124,7 @@ PHP_METHOD(DOMNode, replaceChild)
11241124
nodep->doc->intSubset = (xmlDtd *) newchild;
11251125
}
11261126
}
1127-
php_libxml_invalidate_node_list_cache_from_doc(nodep->doc);
1127+
php_libxml_invalidate_node_list_cache(intern->document);
11281128
DOM_RET_OBJ(oldchild, &ret, intern);
11291129
}
11301130
/* }}} end dom_node_replace_child */
@@ -1166,7 +1166,7 @@ PHP_METHOD(DOMNode, removeChild)
11661166
}
11671167

11681168
xmlUnlinkNode(child);
1169-
php_libxml_invalidate_node_list_cache_from_doc(nodep->doc);
1169+
php_libxml_invalidate_node_list_cache(intern->document);
11701170
DOM_RET_OBJ(child, &ret, intern);
11711171
}
11721172
/* }}} end dom_node_remove_child */
@@ -1271,7 +1271,7 @@ PHP_METHOD(DOMNode, appendChild)
12711271
dom_reconcile_ns(nodep->doc, new_child);
12721272
}
12731273

1274-
php_libxml_invalidate_node_list_cache_from_doc(nodep->doc);
1274+
php_libxml_invalidate_node_list_cache(intern->document);
12751275

12761276
DOM_RET_OBJ(new_child, &ret, intern);
12771277
return;
@@ -1387,7 +1387,7 @@ PHP_METHOD(DOMNode, normalize)
13871387

13881388
DOM_GET_OBJ(nodep, id, xmlNodePtr, intern);
13891389

1390-
php_libxml_invalidate_node_list_cache_from_doc(nodep->doc);
1390+
php_libxml_invalidate_node_list_cache(intern->document);
13911391

13921392
dom_normalize(nodep);
13931393

ext/dom/parentnode.c

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ void dom_parent_node_append(dom_object *context, zval *nodes, uint32_t nodesc)
309309
return;
310310
}
311311

312-
php_libxml_invalidate_node_list_cache_from_doc(parentNode->doc);
312+
php_libxml_invalidate_node_list_cache(context->document);
313313

314314
xmlNode *fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
315315

@@ -353,7 +353,7 @@ void dom_parent_node_prepend(dom_object *context, zval *nodes, uint32_t nodesc)
353353
return;
354354
}
355355

356-
php_libxml_invalidate_node_list_cache_from_doc(parentNode->doc);
356+
php_libxml_invalidate_node_list_cache(context->document);
357357

358358
xmlNode *fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
359359

@@ -404,7 +404,7 @@ void dom_parent_node_after(dom_object *context, zval *nodes, uint32_t nodesc)
404404

405405
doc = prevsib->doc;
406406

407-
php_libxml_invalidate_node_list_cache_from_doc(doc);
407+
php_libxml_invalidate_node_list_cache(context->document);
408408

409409
/* Spec step 4: convert nodes into fragment */
410410
fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
@@ -456,7 +456,7 @@ void dom_parent_node_before(dom_object *context, zval *nodes, uint32_t nodesc)
456456

457457
doc = nextsib->doc;
458458

459-
php_libxml_invalidate_node_list_cache_from_doc(doc);
459+
php_libxml_invalidate_node_list_cache(context->document);
460460

461461
/* Spec step 4: convert nodes into fragment */
462462
fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
@@ -523,7 +523,7 @@ void dom_child_node_remove(dom_object *context)
523523
return;
524524
}
525525

526-
php_libxml_invalidate_node_list_cache_from_doc(child->doc);
526+
php_libxml_invalidate_node_list_cache(context->document);
527527

528528
xmlUnlinkNode(child);
529529
}
@@ -557,7 +557,7 @@ void dom_child_replace_with(dom_object *context, zval *nodes, uint32_t nodesc)
557557
}
558558

559559
xmlDocPtr doc = parentNode->doc;
560-
php_libxml_invalidate_node_list_cache_from_doc(doc);
560+
php_libxml_invalidate_node_list_cache(context->document);
561561

562562
/* Spec step 4: convert nodes into fragment */
563563
xmlNodePtr fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
@@ -602,7 +602,7 @@ void dom_parent_node_replace_children(dom_object *context, zval *nodes, uint32_t
602602
return;
603603
}
604604

605-
php_libxml_invalidate_node_list_cache_from_doc(thisp->doc);
605+
php_libxml_invalidate_node_list_cache(context->document);
606606

607607
dom_remove_all_children(thisp);
608608

ext/dom/php_dom.h

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ int php_dom_get_nodelist_length(dom_object *obj);
200200
#define DOM_NODELIST 0
201201
#define DOM_NAMEDNODEMAP 1
202202

203-
static zend_always_inline bool php_dom_is_cache_tag_stale_from_doc_ptr(const php_libxml_cache_tag *cache_tag, const php_libxml_doc_ptr *doc_ptr)
203+
static zend_always_inline bool php_dom_is_cache_tag_stale_from_doc_ptr(const php_libxml_cache_tag *cache_tag, const php_libxml_ref_obj *doc_ptr)
204204
{
205205
ZEND_ASSERT(cache_tag != NULL);
206206
ZEND_ASSERT(doc_ptr != NULL);
@@ -215,15 +215,26 @@ static zend_always_inline bool php_dom_is_cache_tag_stale_from_doc_ptr(const php
215215
static zend_always_inline bool php_dom_is_cache_tag_stale_from_node(const php_libxml_cache_tag *cache_tag, const xmlNodePtr node)
216216
{
217217
ZEND_ASSERT(node != NULL);
218-
return !node->doc || !node->doc->_private || php_dom_is_cache_tag_stale_from_doc_ptr(cache_tag, node->doc->_private);
218+
php_libxml_node_ptr *private = node->_private;
219+
if (!private) {
220+
return true;
221+
}
222+
php_libxml_node_object *object_private = private->_private;
223+
if (!object_private || !object_private->document) {
224+
return true;
225+
}
226+
return php_dom_is_cache_tag_stale_from_doc_ptr(cache_tag, object_private->document);
219227
}
220228

221229
static zend_always_inline void php_dom_mark_cache_tag_up_to_date_from_node(php_libxml_cache_tag *cache_tag, const xmlNodePtr node)
222230
{
223231
ZEND_ASSERT(cache_tag != NULL);
224-
if (node->doc && node->doc->_private) {
225-
const php_libxml_doc_ptr* doc_ptr = node->doc->_private;
226-
cache_tag->modification_nr = doc_ptr->cache_tag.modification_nr;
232+
php_libxml_node_ptr *private = node->_private;
233+
if (private) {
234+
php_libxml_node_object *object_private = private->_private;
235+
if (object_private->document) {
236+
cache_tag->modification_nr = object_private->document->cache_tag.modification_nr;
237+
}
227238
}
228239
}
229240

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
getElementsByTagName() liveness with deallocated document
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
$dom = new DOMDocument;
9+
$dom->loadXML(<<<XML
10+
<?xml version="1.0"?>
11+
<container>
12+
<p>1</p><p>2</p><p>3</p>
13+
</container>
14+
XML);
15+
16+
$ps = $dom->documentElement->getElementsByTagName('p');
17+
$second = $ps->item(1);
18+
var_dump($second->textContent);
19+
var_dump($ps->length);
20+
21+
unset($dom);
22+
$dom = $second->ownerDocument;
23+
24+
$second->parentNode->appendChild($dom->createElement('p', '4'));
25+
var_dump($ps->length);
26+
27+
?>
28+
--EXPECT--
29+
string(1) "2"
30+
int(3)
31+
int(4)

ext/libxml/libxml.c

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,13 +1293,7 @@ PHP_LIBXML_API int php_libxml_increment_node_ptr(php_libxml_node_object *object,
12931293
object->node->_private = private_data;
12941294
}
12951295
} else {
1296-
if (UNEXPECTED(node->type == XML_DOCUMENT_NODE || node->type == XML_HTML_DOCUMENT_NODE)) {
1297-
php_libxml_doc_ptr *doc_ptr = emalloc(sizeof(php_libxml_doc_ptr));
1298-
doc_ptr->cache_tag.modification_nr = 1; /* iterators start at 0, such that they will start in an uninitialised state */
1299-
object->node = (php_libxml_node_ptr *) doc_ptr; /* downcast */
1300-
} else {
1301-
object->node = emalloc(sizeof(php_libxml_node_ptr));
1302-
}
1296+
object->node = emalloc(sizeof(php_libxml_node_ptr));
13031297
ret_refcount = 1;
13041298
object->node->node = node;
13051299
object->node->refcount = 1;
@@ -1344,6 +1338,7 @@ PHP_LIBXML_API int php_libxml_increment_doc_ref(php_libxml_node_object *object,
13441338
object->document->ptr = docp;
13451339
object->document->refcount = ret_refcount;
13461340
object->document->doc_props = NULL;
1341+
object->document->cache_tag.modification_nr = 1; /* iterators start at 0, such that they will start in an uninitialised state */
13471342
}
13481343

13491344
return ret_refcount;

ext/libxml/php_libxml.h

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,15 @@ typedef struct _libxml_doc_props {
5757
bool recover;
5858
} libxml_doc_props;
5959

60+
typedef struct {
61+
size_t modification_nr;
62+
} php_libxml_cache_tag;
63+
6064
typedef struct _php_libxml_ref_obj {
6165
void *ptr;
6266
int refcount;
6367
libxml_doc_props *doc_props;
68+
php_libxml_cache_tag cache_tag;
6469
} php_libxml_ref_obj;
6570

6671
typedef struct _php_libxml_node_ptr {
@@ -69,16 +74,6 @@ typedef struct _php_libxml_node_ptr {
6974
void *_private;
7075
} php_libxml_node_ptr;
7176

72-
typedef struct {
73-
size_t modification_nr;
74-
} php_libxml_cache_tag;
75-
76-
/* extends php_libxml_node_ptr */
77-
typedef struct {
78-
php_libxml_node_ptr node_ptr;
79-
php_libxml_cache_tag cache_tag;
80-
} php_libxml_doc_ptr;
81-
8277
typedef struct _php_libxml_node_object {
8378
php_libxml_node_ptr *node;
8479
php_libxml_ref_obj *document;
@@ -91,8 +86,11 @@ static inline php_libxml_node_object *php_libxml_node_fetch_object(zend_object *
9186
return (php_libxml_node_object *)((char*)(obj) - obj->handlers->offset);
9287
}
9388

94-
static zend_always_inline void php_libxml_invalidate_node_list_cache(php_libxml_doc_ptr *doc_ptr)
89+
static zend_always_inline void php_libxml_invalidate_node_list_cache(php_libxml_ref_obj *doc_ptr)
9590
{
91+
if (!doc_ptr) {
92+
return;
93+
}
9694
#if SIZEOF_SIZE_T == 8
9795
/* If one operation happens every nanosecond, then it would still require 584 years to overflow
9896
* the counter. So we'll just assume this never happens. */
@@ -108,7 +106,11 @@ static zend_always_inline void php_libxml_invalidate_node_list_cache(php_libxml_
108106
static zend_always_inline void php_libxml_invalidate_node_list_cache_from_doc(xmlDocPtr docp)
109107
{
110108
if (docp && docp->_private) { /* docp is NULL for detached nodes */
111-
php_libxml_invalidate_node_list_cache((php_libxml_doc_ptr *)docp->_private);
109+
php_libxml_node_ptr *private = docp->_private;
110+
php_libxml_node_object *object_private = private->_private;
111+
if (object_private) {
112+
php_libxml_invalidate_node_list_cache(object_private->document);
113+
}
112114
}
113115
}
114116

0 commit comments

Comments
 (0)