Skip to content

Commit 3b5eeb9

Browse files
committed
ext/dom: resolve in-scope prefixed QName values during document validation.
Fix #22219 Modern DOM keeps namespace declarations off the tree (node->nsDef is NULL), so libxml's native validators cannot resolve a prefixed QName appearing in element or attribute content. Temporarily materialize them as nsDef entries around schema, RelaxNG and DTD validation, reusing the C14N relink machinery, then restore the tree. close GH-22224
1 parent 9e68cf8 commit 3b5eeb9

6 files changed

Lines changed: 243 additions & 142 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ PHP NEWS
1010
. Fix incorrect recurrence check of DatePeriod::createFromISO8601String().
1111
(ndossche)
1212

13+
- DOM:
14+
. Fix GH-22219 (Dom\XMLDocument::schemaValidate fails to resolve
15+
xs:QName with prefix from imported schema). (David Carlier)
16+
1317
- GD:
1418
. Fixed bug GH-22121 (Double free in gdImageSetStyle() after
1519
overflow-triggered early return). (iliaal)

ext/dom/document.c

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,6 +1757,35 @@ static int dom_perform_xinclude(xmlDocPtr docp, dom_object *intern, zend_long fl
17571757
return err;
17581758
}
17591759

1760+
/* For modern DOM, namespace declarations are stored as attributes (node->nsDef
1761+
* is NULL), so libxml's native validators can't resolve prefixed QNames found in
1762+
* content (e.g. an xs:QName attribute value). Temporarily relink them, mirroring
1763+
* what C14N does in dom_canonicalization(). */
1764+
typedef struct {
1765+
HashTable links;
1766+
bool active;
1767+
} dom_validate_ns_guard;
1768+
1769+
static void dom_validate_ns_guard_begin(dom_validate_ns_guard *guard, xmlDocPtr docp)
1770+
{
1771+
guard->active = php_dom_follow_spec_node((const xmlNode *) docp);
1772+
if (guard->active) {
1773+
zend_hash_init(&guard->links, 0, NULL, NULL, false);
1774+
xmlNodePtr root_element = xmlDocGetRootElement(docp);
1775+
if (root_element) {
1776+
dom_relink_ns_decls(&guard->links, root_element);
1777+
}
1778+
}
1779+
}
1780+
1781+
static void dom_validate_ns_guard_end(dom_validate_ns_guard *guard)
1782+
{
1783+
if (guard->active) {
1784+
dom_unlink_ns_decls(&guard->links);
1785+
zend_hash_destroy(&guard->links);
1786+
}
1787+
}
1788+
17601789
/* {{{ Substitutues xincludes in a DomDocument */
17611790
PHP_METHOD(DOMDocument, xinclude)
17621791
{
@@ -1832,8 +1861,11 @@ PHP_METHOD(DOMDocument, validate)
18321861
cvp->userData = NULL;
18331862
cvp->error = (xmlValidityErrorFunc) php_libxml_error_handler;
18341863
cvp->warning = (xmlValidityErrorFunc) php_libxml_error_handler;
1835-
1836-
if (xmlValidateDocument(cvp, docp)) {
1864+
dom_validate_ns_guard guard;
1865+
dom_validate_ns_guard_begin(&guard, docp);
1866+
int dtd_valid = xmlValidateDocument(cvp, docp);
1867+
dom_validate_ns_guard_end(&guard);
1868+
if (dtd_valid) {
18371869
RETVAL_TRUE;
18381870
} else {
18391871
RETVAL_FALSE;
@@ -1930,7 +1962,10 @@ static void dom_document_schema_validate(INTERNAL_FUNCTION_PARAMETERS, int type)
19301962
PHP_LIBXML_SANITIZE_GLOBALS(validate);
19311963
xmlSchemaSetValidOptions(vptr, valid_opts);
19321964
xmlSchemaSetValidErrors(vptr, php_libxml_error_handler, php_libxml_error_handler, vptr);
1965+
dom_validate_ns_guard guard;
1966+
dom_validate_ns_guard_begin(&guard, docp);
19331967
is_valid = xmlSchemaValidateDoc(vptr, docp);
1968+
dom_validate_ns_guard_end(&guard);
19341969
xmlSchemaFree(sptr);
19351970
xmlSchemaFreeValidCtxt(vptr);
19361971
PHP_LIBXML_RESTORE_GLOBALS(validate);
@@ -2028,7 +2063,10 @@ static void dom_document_relaxNG_validate(INTERNAL_FUNCTION_PARAMETERS, int type
20282063
}
20292064

20302065
xmlRelaxNGSetValidErrors(vptr, php_libxml_error_handler, php_libxml_error_handler, vptr);
2066+
dom_validate_ns_guard guard;
2067+
dom_validate_ns_guard_begin(&guard, docp);
20312068
is_valid = xmlRelaxNGValidateDoc(vptr, docp);
2069+
dom_validate_ns_guard_end(&guard);
20322070
xmlRelaxNGFree(sptr);
20332071
xmlRelaxNGFreeValidCtxt(vptr);
20342072

ext/dom/namespace_compat.c

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,4 +501,142 @@ PHP_DOM_EXPORT void php_dom_in_scope_ns_destroy(php_dom_in_scope_ns *in_scope_ns
501501
}
502502
}
503503

504+
static xmlNsPtr dom_alloc_ns_decl(HashTable *links, xmlNodePtr node)
505+
{
506+
xmlNsPtr ns = xmlMalloc(sizeof(*ns));
507+
if (!ns) {
508+
return NULL;
509+
}
510+
511+
zval *zv = zend_hash_index_lookup(links, (zend_ulong) node);
512+
if (Z_ISNULL_P(zv)) {
513+
ZVAL_LONG(zv, 1);
514+
} else {
515+
Z_LVAL_P(zv)++;
516+
}
517+
518+
memset(ns, 0, sizeof(*ns));
519+
ns->type = XML_LOCAL_NAMESPACE;
520+
ns->next = node->nsDef;
521+
node->nsDef = ns;
522+
523+
return ns;
524+
}
525+
526+
/* Mint a temporary nsDef entry so C14N finds namespaces that live on node->ns
527+
* but have no matching xmlns attribute (typical for createElementNS). */
528+
static void dom_add_synthetic_ns_decl(HashTable *links, xmlNodePtr node, xmlNsPtr src_ns)
529+
{
530+
xmlNsPtr ns = dom_alloc_ns_decl(links, node);
531+
if (!ns) {
532+
return;
533+
}
534+
535+
ns->href = xmlStrdup(src_ns->href);
536+
ns->prefix = src_ns->prefix ? xmlStrdup(src_ns->prefix) : NULL;
537+
}
538+
539+
/* Same, but for attribute namespaces, which may collide by prefix with the
540+
* element's own ns or with a sibling attribute's ns. */
541+
static void dom_add_synthetic_ns_decl_for_attr(HashTable *links, xmlNodePtr node, xmlNsPtr src_ns)
542+
{
543+
for (xmlNsPtr existing = node->nsDef; existing; existing = existing->next) {
544+
if (xmlStrEqual(existing->prefix, src_ns->prefix)) {
545+
return;
546+
}
547+
}
548+
549+
dom_add_synthetic_ns_decl(links, node, src_ns);
550+
}
551+
552+
static void dom_relink_ns_decls_element(HashTable *links, xmlNodePtr node)
553+
{
554+
if (node->type == XML_ELEMENT_NODE) {
555+
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) {
556+
if (php_dom_ns_is_fast((const xmlNode *) attr, php_dom_ns_is_xmlns_magic_token)) {
557+
xmlNsPtr ns = dom_alloc_ns_decl(links, node);
558+
if (!ns) {
559+
return;
560+
}
561+
562+
bool should_free;
563+
xmlChar *attr_value = php_libxml_attr_value(attr, &should_free);
564+
565+
ns->href = should_free ? attr_value : xmlStrdup(attr_value);
566+
ns->prefix = attr->ns->prefix ? xmlStrdup(attr->name) : NULL;
567+
ns->_private = attr;
568+
if (attr->prev) {
569+
attr->prev->next = attr->next;
570+
} else {
571+
node->properties = attr->next;
572+
}
573+
if (attr->next) {
574+
attr->next->prev = attr->prev;
575+
}
576+
}
577+
}
578+
579+
/* The default namespace is handled separately from the other namespaces in C14N.
580+
* The default namespace is explicitly looked up while the other namespaces are
581+
* deduplicated and compared to a list of visible namespaces. */
582+
if (node->ns && !node->ns->prefix) {
583+
/* Workaround for the behaviour where the xmlSearchNs() call inside c14n.c
584+
* can return the current namespace. */
585+
zend_hash_index_add_new_ptr(links, (zend_ulong) node | 1, node->ns);
586+
node->ns = xmlSearchNs(node->doc, node, NULL);
587+
} else if (node->ns) {
588+
dom_add_synthetic_ns_decl(links, node, node->ns);
589+
}
590+
591+
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) {
592+
if (attr->ns && !php_dom_ns_is_fast((const xmlNode *) attr, php_dom_ns_is_xmlns_magic_token)) {
593+
dom_add_synthetic_ns_decl_for_attr(links, node, attr->ns);
594+
}
595+
}
596+
}
597+
}
598+
599+
void dom_relink_ns_decls(HashTable *links, xmlNodePtr root)
600+
{
601+
dom_relink_ns_decls_element(links, root);
602+
603+
xmlNodePtr base = root;
604+
xmlNodePtr node = base->children;
605+
while (node != NULL) {
606+
dom_relink_ns_decls_element(links, node);
607+
node = php_dom_next_in_tree_order(node, base);
608+
}
609+
}
610+
611+
void dom_unlink_ns_decls(HashTable *links)
612+
{
613+
ZEND_HASH_MAP_FOREACH_NUM_KEY_VAL(links, zend_ulong h, zval *data) {
614+
if (h & 1) {
615+
xmlNodePtr node = (xmlNodePtr) (h ^ 1);
616+
node->ns = Z_PTR_P(data);
617+
} else {
618+
xmlNodePtr node = (xmlNodePtr) h;
619+
while (Z_LVAL_P(data)-- > 0) {
620+
xmlNsPtr ns = node->nsDef;
621+
node->nsDef = ns->next;
622+
623+
xmlAttrPtr attr = ns->_private;
624+
if (attr) {
625+
if (attr->prev) {
626+
attr->prev->next = attr;
627+
} else {
628+
node->properties = attr;
629+
}
630+
if (attr->next) {
631+
attr->next->prev = attr;
632+
}
633+
}
634+
635+
xmlFreeNs(ns);
636+
}
637+
}
638+
} ZEND_HASH_FOREACH_END();
639+
}
640+
641+
504642
#endif /* HAVE_LIBXML && HAVE_DOM */

ext/dom/node.c

Lines changed: 0 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -2103,146 +2103,6 @@ PHP_METHOD(DOMNode, lookupNamespaceURI)
21032103
}
21042104
/* }}} end dom_node_lookup_namespace_uri */
21052105

2106-
/* Allocate, track and prepend a temporary nsDef entry for C14N.
2107-
* Returns the new xmlNsPtr for the caller to fill in href/prefix/_private,
2108-
* or NULL on allocation failure. */
2109-
static xmlNsPtr dom_alloc_ns_decl(HashTable *links, xmlNodePtr node)
2110-
{
2111-
xmlNsPtr ns = xmlMalloc(sizeof(*ns));
2112-
if (!ns) {
2113-
return NULL;
2114-
}
2115-
2116-
zval *zv = zend_hash_index_lookup(links, (zend_ulong) node);
2117-
if (Z_ISNULL_P(zv)) {
2118-
ZVAL_LONG(zv, 1);
2119-
} else {
2120-
Z_LVAL_P(zv)++;
2121-
}
2122-
2123-
memset(ns, 0, sizeof(*ns));
2124-
ns->type = XML_LOCAL_NAMESPACE;
2125-
ns->next = node->nsDef;
2126-
node->nsDef = ns;
2127-
2128-
return ns;
2129-
}
2130-
2131-
/* Mint a temporary nsDef entry so C14N finds namespaces that live on node->ns
2132-
* but have no matching xmlns attribute (typical for createElementNS). */
2133-
static void dom_add_synthetic_ns_decl(HashTable *links, xmlNodePtr node, xmlNsPtr src_ns)
2134-
{
2135-
xmlNsPtr ns = dom_alloc_ns_decl(links, node);
2136-
if (!ns) {
2137-
return;
2138-
}
2139-
2140-
ns->href = xmlStrdup(src_ns->href);
2141-
ns->prefix = src_ns->prefix ? xmlStrdup(src_ns->prefix) : NULL;
2142-
}
2143-
2144-
/* Same, but for attribute namespaces, which may collide by prefix with the
2145-
* element's own ns or with a sibling attribute's ns. */
2146-
static void dom_add_synthetic_ns_decl_for_attr(HashTable *links, xmlNodePtr node, xmlNsPtr src_ns)
2147-
{
2148-
for (xmlNsPtr existing = node->nsDef; existing; existing = existing->next) {
2149-
if (xmlStrEqual(existing->prefix, src_ns->prefix)) {
2150-
return;
2151-
}
2152-
}
2153-
2154-
dom_add_synthetic_ns_decl(links, node, src_ns);
2155-
}
2156-
2157-
static void dom_relink_ns_decls_element(HashTable *links, xmlNodePtr node)
2158-
{
2159-
if (node->type == XML_ELEMENT_NODE) {
2160-
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) {
2161-
if (php_dom_ns_is_fast((const xmlNode *) attr, php_dom_ns_is_xmlns_magic_token)) {
2162-
xmlNsPtr ns = dom_alloc_ns_decl(links, node);
2163-
if (!ns) {
2164-
return;
2165-
}
2166-
2167-
bool should_free;
2168-
xmlChar *attr_value = php_libxml_attr_value(attr, &should_free);
2169-
2170-
ns->href = should_free ? attr_value : xmlStrdup(attr_value);
2171-
ns->prefix = attr->ns->prefix ? xmlStrdup(attr->name) : NULL;
2172-
ns->_private = attr;
2173-
if (attr->prev) {
2174-
attr->prev->next = attr->next;
2175-
} else {
2176-
node->properties = attr->next;
2177-
}
2178-
if (attr->next) {
2179-
attr->next->prev = attr->prev;
2180-
}
2181-
}
2182-
}
2183-
2184-
/* The default namespace is handled separately from the other namespaces in C14N.
2185-
* The default namespace is explicitly looked up while the other namespaces are
2186-
* deduplicated and compared to a list of visible namespaces. */
2187-
if (node->ns && !node->ns->prefix) {
2188-
/* Workaround for the behaviour where the xmlSearchNs() call inside c14n.c
2189-
* can return the current namespace. */
2190-
zend_hash_index_add_new_ptr(links, (zend_ulong) node | 1, node->ns);
2191-
node->ns = xmlSearchNs(node->doc, node, NULL);
2192-
} else if (node->ns) {
2193-
dom_add_synthetic_ns_decl(links, node, node->ns);
2194-
}
2195-
2196-
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) {
2197-
if (attr->ns && !php_dom_ns_is_fast((const xmlNode *) attr, php_dom_ns_is_xmlns_magic_token)) {
2198-
dom_add_synthetic_ns_decl_for_attr(links, node, attr->ns);
2199-
}
2200-
}
2201-
}
2202-
}
2203-
2204-
static void dom_relink_ns_decls(HashTable *links, xmlNodePtr root)
2205-
{
2206-
dom_relink_ns_decls_element(links, root);
2207-
2208-
xmlNodePtr base = root;
2209-
xmlNodePtr node = base->children;
2210-
while (node != NULL) {
2211-
dom_relink_ns_decls_element(links, node);
2212-
node = php_dom_next_in_tree_order(node, base);
2213-
}
2214-
}
2215-
2216-
static void dom_unlink_ns_decls(HashTable *links)
2217-
{
2218-
ZEND_HASH_MAP_FOREACH_NUM_KEY_VAL(links, zend_ulong h, zval *data) {
2219-
if (h & 1) {
2220-
xmlNodePtr node = (xmlNodePtr) (h ^ 1);
2221-
node->ns = Z_PTR_P(data);
2222-
} else {
2223-
xmlNodePtr node = (xmlNodePtr) h;
2224-
while (Z_LVAL_P(data)-- > 0) {
2225-
xmlNsPtr ns = node->nsDef;
2226-
node->nsDef = ns->next;
2227-
2228-
xmlAttrPtr attr = ns->_private;
2229-
if (attr) {
2230-
if (attr->prev) {
2231-
attr->prev->next = attr;
2232-
} else {
2233-
node->properties = attr;
2234-
}
2235-
if (attr->next) {
2236-
attr->next->prev = attr;
2237-
}
2238-
}
2239-
2240-
xmlFreeNs(ns);
2241-
}
2242-
}
2243-
} ZEND_HASH_FOREACH_END();
2244-
}
2245-
22462106
static int dom_canonicalize_node_parent_lookup_cb(void *user_data, xmlNodePtr node, xmlNodePtr parent)
22472107
{
22482108
xmlNodePtr root = user_data;

ext/dom/php_dom.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,13 @@ bool php_dom_create_nullable_object(xmlNodePtr obj, zval *return_value, dom_obje
187187
xmlNodePtr dom_clone_node(php_dom_libxml_ns_mapper *ns_mapper, xmlNodePtr node, xmlDocPtr doc, bool recursive);
188188
void dom_set_document_ref_pointers(xmlNodePtr node, php_libxml_ref_obj *document);
189189
void dom_set_document_ref_pointers_attr(xmlAttrPtr attr, php_libxml_ref_obj *document);
190+
191+
/* Temporarily materialize namespace declarations as nsDef entries on the tree so
192+
* that libxml's native validators/canonicalizers can resolve prefixed QNames that
193+
* appear in element/attribute *content*. Modern DOM keeps declarations off the
194+
* tree (node->nsDef == NULL), which xmlSearchNs() cannot follow. Internal only. */
195+
void dom_relink_ns_decls(HashTable *links, xmlNodePtr root);
196+
void dom_unlink_ns_decls(HashTable *links);
190197
zval *dom_element_class_list_zval(dom_object *obj);
191198

192199
typedef enum {

0 commit comments

Comments
 (0)