Coverage Report

Created: 2026-09-29 06:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pacemaker/lib/common/xpath.c
Line
Count
Source
1
/*
2
 * Copyright 2004-2026 the Pacemaker project contributors
3
 *
4
 * The version control history for this file may have further details.
5
 *
6
 * This source code is licensed under the GNU Lesser General Public License
7
 * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
8
 */
9
10
#include <crm_internal.h>
11
#include <stdint.h>                     // uint8_t
12
#include <stdio.h>
13
#include <string.h>
14
15
#include <libxml/tree.h>                // xmlNode
16
#include <libxml/xmlstring.h>           // xmlChar
17
#include <libxml/xpath.h>               // xmlXPathObject, etc.
18
19
#include <crm/common/xml.h>
20
#include "crmcommon_private.h"
21
22
/*!
23
 * \internal
24
 * \brief Get a node from the result set of evaluating an XPath expression
25
 *
26
 * Evaluating an XPath expression stores the list of matching nodes in an
27
 * \c xmlXPathObject. This function gets the node at a particular index within
28
 * that list.
29
 *
30
 * \param[in,out] xpath_obj  XPath object containing result nodes
31
 * \param[in]     index      Index of result node to get
32
 *
33
 * \return Result node at the given index if possible, or \c NULL otherwise
34
 *
35
 * \note This has a side effect: it sets the result node at \p index to NULL
36
 *       within \p xpath_obj, so the result at a given index can be retrieved
37
 *       only once. This is a workaround to prevent a use-after-free error.
38
 *
39
 *       All elements returned by an XPath query are pointers to elements from
40
 *       the tree, except namespace nodes (which are allocated separately for
41
 *       the XPath object's node set). Accordingly, only namespace nodes and the
42
 *       node set itself are freed when libxml2 frees a node set.
43
 *
44
 *       This logic requires checking the type of every node in the node set.
45
 *       However, a node may have been freed already while processing an XPath
46
 *       object -- either directly (for example, with \c xmlFreeNode()) or
47
 *       indirectly (for example, with \c xmlNodeSetContent()). In that case,
48
 *       checking the freed node's type while freeing the XPath object is a
49
 *       use-after-free error.
50
 *
51
 *       To reduce the likelihood of this, when we access a node in the XPath
52
 *       object, we remove it from the XPath object's node set by setting it to
53
 *       \c NULL. This approach is adapted from \c xpath2.c in libxml2's
54
 *       examples. That file also describes a way to reproduce the
55
 *       use-after-free error.
56
 *
57
 *       However, there are still ways that a use-after-free can occur. For
58
 *       example, freeing the entire XML tree before freeing an XPath object
59
 *       that contains pointers to it would be an error. It's dangerous to mix
60
 *       processing XPath search results with modifications to a tree, and it
61
 *       must be done with care.
62
 */
63
xmlNode *
64
pcmk__xpath_result(xmlXPathObject *xpath_obj, int index)
65
0
{
66
0
    xmlNode *match = NULL;
67
68
0
    CRM_CHECK((xpath_obj != NULL) && (index >= 0), return NULL);
69
70
0
    match = xmlXPathNodeSetItem(xpath_obj->nodesetval, index);
71
0
    if (match == NULL) {
72
        // Previously requested or out of range
73
0
        return NULL;
74
0
    }
75
76
0
    if (match->type != XML_NAMESPACE_DECL) {
77
0
        xpath_obj->nodesetval->nodeTab[index] = NULL;
78
0
    }
79
80
0
    return match;
81
0
}
82
83
/*!
84
 * \internal
85
 * \brief Get an element node corresponding to an XPath match node
86
 *
87
 * Each node in an XPath object's result node set may be of an arbitrary type.
88
 * This function is guaranteed to return an element node (or \c NULL).
89
 *
90
 * \param[in] match  XML node that matched some XPath expression
91
 *
92
 * \retval \p match if \p match is an element
93
 * \retval Root element of \p match if \p match is a document
94
 * \retval <tt>match->parent</tt> if \p match is not an element but its parent
95
 *         is an element
96
 * \retval \c NULL otherwise
97
 *
98
 * \todo Phase this out. Code that relies on this behavior is likely buggy.
99
 */
100
xmlNode *
101
pcmk__xpath_match_element(xmlNode *match)
102
0
{
103
0
    pcmk__assert(match != NULL);
104
105
0
    switch (match->type) {
106
0
        case XML_ELEMENT_NODE:
107
0
            return match;
108
109
0
        case XML_DOCUMENT_NODE:
110
            // Happens if XPath expression is "/"; return root element instead
111
0
            return xmlDocGetRootElement((xmlDoc *) match);
112
113
0
        default:
114
0
            if ((match->parent != NULL)
115
0
                && (match->parent->type == XML_ELEMENT_NODE)) {
116
117
                // Probably an attribute; return parent element instead
118
0
                return match->parent;
119
0
            }
120
0
            pcmk__err("Cannot get element from XPath expression match of type "
121
0
                      "%s",
122
0
                      pcmk__xml_element_type_text(match->type));
123
0
            return NULL;
124
0
    }
125
0
}
126
127
/*!
128
 * \internal
129
 * \brief Search an XML document using an XPath expression
130
 *
131
 * \param[in] doc   XML document to search
132
 * \param[in] path  XPath expression to evaluate in the context of \p doc
133
 *
134
 * \return XPath object containing result of evaluating \p path against \p doc
135
 */
136
xmlXPathObject *
137
pcmk__xpath_search(xmlDoc *doc, const char *path)
138
0
{
139
0
    const xmlChar *xpath_expr = (const xmlChar *) path;
140
0
    xmlXPathContext *xpath_context = NULL;
141
0
    xmlXPathObject *xpath_obj = NULL;
142
143
0
    CRM_CHECK((doc != NULL) && !pcmk__str_empty(path), return NULL);
144
145
0
    xpath_context = xmlXPathNewContext(doc);
146
0
    pcmk__mem_assert(xpath_context);
147
148
0
    xpath_obj = xmlXPathEval(xpath_expr, xpath_context);
149
150
0
    xmlXPathFreeContext(xpath_context);
151
0
    return xpath_obj;
152
0
}
153
154
/*!
155
 * \internal
156
 * \brief Run a supplied function for each result of an XPath search
157
 *
158
 * \param[in,out] doc        XML document to search
159
 * \param[in]     path       XPath expression to evaluate in the context of
160
 *                           \p doc
161
 * \param[in]     fn         Function to call for each result XML element
162
 * \param[in,out] user_data  Data to pass to \p fn
163
 *
164
 * \note This function processes the result node set in forward order. If \p fn
165
 *       may free any part of any result node, then it is safer to process the
166
 *       result node set in reverse order. (The node set is in document order.)
167
 *       See comments in libxml's <tt>examples/xpath2.c</tt> file.
168
 */
169
void
170
pcmk__xpath_foreach_result(xmlDoc *doc, const char *path,
171
                           void (*fn)(xmlNode *, void *), void *user_data)
172
0
{
173
0
    xmlXPathObject *xpath_obj = NULL;
174
0
    int num_results = 0;
175
176
0
    pcmk__assert(fn != NULL);
177
0
    CRM_CHECK((doc != NULL) && !pcmk__str_empty(path), return);
178
179
0
    xpath_obj = pcmk__xpath_search(doc, path);
180
0
    num_results = pcmk__xpath_num_results(xpath_obj);
181
182
0
    for (int i = 0; i < num_results; i++) {
183
0
        xmlNode *result = pcmk__xpath_result(xpath_obj, i);
184
185
0
        if (result != NULL) {
186
0
            fn(result, user_data);
187
0
        }
188
0
    }
189
0
    xmlXPathFreeObject(xpath_obj);
190
0
}
191
192
/*!
193
 * \internal
194
 * \brief Search an XML document using an XPath expression and get result node
195
 *
196
 * This function requires a unique result node from evaluating the XPath
197
 * expression. If there are multiple result nodes or no result nodes, it returns
198
 * \c NULL.
199
 *
200
 * \param[in] doc    XML document to search
201
 * \param[in] path   XPath expression to evaluate in the context of \p doc
202
 * \param[in] level  Log level for errors
203
 *
204
 * \return Result node from evaluating \p path if unique, or \c NULL otherwise
205
 */
206
xmlNode *
207
pcmk__xpath_find_one(xmlDoc *doc, const char *path, uint8_t level)
208
0
{
209
0
    int num_results = 0;
210
0
    xmlNode *result = NULL;
211
0
    xmlXPathObject *xpath_obj = NULL;
212
0
    const xmlNode *root = NULL;
213
0
    const char *root_name = "(unknown)";
214
215
0
    CRM_CHECK((doc != NULL) && (path != NULL), goto done);
216
217
0
    xpath_obj = pcmk__xpath_search(doc, path);
218
0
    num_results = pcmk__xpath_num_results(xpath_obj);
219
220
0
    if (num_results == 1) {
221
0
        result = pcmk__xpath_result(xpath_obj, 0);
222
0
        goto done;
223
0
    }
224
225
0
    if (level >= PCMK__LOG_NEVER) {
226
        // For no matches or multiple matches, the rest is just logging
227
0
        goto done;
228
0
    }
229
230
0
    root = xmlDocGetRootElement(doc);
231
0
    if (root != NULL) {
232
0
        root_name = (const char *) root->name;
233
0
    }
234
235
0
    if (num_results < 1) {
236
0
        do_crm_log(level, "No match for %s in <%s>", path, root_name);
237
238
0
        if (root != NULL) {
239
0
            crm_log_xml_explicit(root, "no-match");
240
0
        }
241
0
        goto done;
242
0
    }
243
244
0
    do_crm_log(level, "Multiple matches for %s in <%s>", path, root_name);
245
246
0
    for (int i = 0; i < num_results; i++) {
247
0
        xmlNode *match = pcmk__xpath_result(xpath_obj, i);
248
0
        xmlChar *match_path = NULL;
249
250
0
        if (match == NULL) {
251
0
            CRM_LOG_ASSERT(match != NULL);
252
0
            continue;
253
0
        }
254
255
0
        match_path = xmlGetNodePath(match);
256
0
        do_crm_log(level, "%s[%d] = %s",
257
0
                   path, i, pcmk__s((const char *) match_path, "(unknown)"));
258
0
        free(match_path);
259
0
    }
260
261
0
    if (root != NULL) {
262
0
        crm_log_xml_explicit(root, "multiple-matches");
263
0
    }
264
265
0
done:
266
0
    xmlXPathFreeObject(xpath_obj);
267
0
    return result;
268
0
}
269
270
/*!
271
 * \internal
272
 * \brief Get an XPath string that matches an XML element as closely as possible
273
 *
274
 * \param[in] xml  The XML element for which to build an XPath string
275
 *
276
 * \return \c GString that matches \p xml, or \c NULL if \p xml is \c NULL
277
 *         (guaranteed not to be \c NULL if \p xml is not \c NULL)
278
 *
279
 * \note The caller is responsible for freeing the string using
280
 *       \c g_string_free().
281
 */
282
GString *
283
pcmk__element_xpath(const xmlNode *xml)
284
0
{
285
0
    const xmlNode *parent = NULL;
286
0
    GString *xpath = NULL;
287
0
    const char *id = NULL;
288
289
0
    if (xml == NULL) {
290
0
        return NULL;
291
0
    }
292
293
0
    parent = xml->parent;
294
0
    xpath = pcmk__element_xpath(parent);
295
0
    if (xpath == NULL) {
296
0
        xpath = g_string_sized_new(256);
297
0
    }
298
299
    // Build xpath like "/" -> "/cib" -> "/cib/configuration"
300
0
    if (parent == NULL) {
301
0
        g_string_append_c(xpath, '/');
302
0
    } else if (parent->parent == NULL) {
303
0
        g_string_append(xpath, (const char *) xml->name);
304
0
    } else {
305
0
        pcmk__g_strcat(xpath, "/", (const char *) xml->name, NULL);
306
0
    }
307
308
0
    id = pcmk__xe_id(xml);
309
0
    if (id != NULL) {
310
0
        pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", id, "']", NULL);
311
0
    }
312
313
0
    return xpath;
314
0
}
315
316
/*!
317
 * \internal
318
 * \brief Extract the ID attribute from an XML element
319
 *
320
 * \param[in] xpath String to search
321
 * \param[in] node  Node to get the ID for
322
 *
323
 * \return ID attribute of \p node in xpath string \p xpath
324
 */
325
char *
326
pcmk__xpath_node_id(const char *xpath, const char *node)
327
0
{
328
0
    char *retval = NULL;
329
0
    char *patt = NULL;
330
0
    const char *start = NULL;
331
0
    const char *end = NULL;
332
333
0
    if (node == NULL || xpath == NULL) {
334
0
        return retval;
335
0
    }
336
337
0
    patt = pcmk__assert_asprintf("/%s[@" PCMK_XA_ID "=", node);
338
339
0
    start = strstr(xpath, patt);
340
0
    if (start == NULL) {
341
0
        goto done;
342
0
    }
343
344
0
    start += strlen(patt);
345
0
    start++;
346
347
0
    end = strchr(start, '\'');
348
0
    pcmk__assert(end != NULL);
349
0
    retval = strndup(start, end-start);
350
351
0
done:
352
0
    free(patt);
353
0
    return retval;
354
0
}
355
356
static int
357
output_attr_child(xmlNode *child, void *userdata)
358
0
{
359
0
    pcmk__output_t *out = userdata;
360
361
0
    out->info(out, "  Value: %s \t(id=%s)",
362
0
              pcmk__xe_get(child, PCMK_XA_VALUE),
363
0
              pcmk__s(pcmk__xe_id(child), "<none>"));
364
0
    return pcmk_rc_ok;
365
0
}
366
367
/*!
368
 * \internal
369
 * \brief Warn if an XPath query returned multiple nodes with the same ID
370
 *
371
 * \param[in,out] out     Output object
372
 * \param[in]     search  XPath search result, most typically the result of
373
 *                        calling <tt>cib->cmds->query()</tt>.
374
 * \param[in]     name    Name searched for
375
 */
376
void
377
pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search,
378
                                 const char *name)
379
0
{
380
0
    if (out == NULL || name == NULL || search == NULL ||
381
0
        search->children == NULL) {
382
0
        return;
383
0
    }
384
385
0
    out->info(out, "Multiple attributes match " PCMK_XA_NAME "=%s", name);
386
0
    pcmk__xe_foreach_child(search, NULL, output_attr_child, out);
387
0
}
388
389
// Deprecated functions kept only for backward API compatibility
390
// LCOV_EXCL_START
391
392
#include <crm/common/xml_compat.h>
393
394
xmlXPathObjectPtr
395
xpath_search(const xmlNode *xml_top, const char *path)
396
0
{
397
0
    CRM_CHECK(xml_top != NULL, return NULL);
398
399
0
    return pcmk__xpath_search(xml_top->doc, path);
400
0
}
401
402
xmlNode *
403
getXpathResult(xmlXPathObjectPtr xpathObj, int index)
404
0
{
405
0
    xmlNode *match = NULL;
406
0
    int max = pcmk__xpath_num_results(xpathObj);
407
408
0
    CRM_CHECK(index >= 0, return NULL);
409
0
    CRM_CHECK(xpathObj != NULL, return NULL);
410
411
0
    if (index >= max) {
412
0
        pcmk__err("Requested index %d of only %d items", index, max);
413
0
        return NULL;
414
415
0
    } else if(xpathObj->nodesetval->nodeTab[index] == NULL) {
416
        /* Previously requested */
417
0
        return NULL;
418
0
    }
419
420
0
    match = xpathObj->nodesetval->nodeTab[index];
421
0
    CRM_CHECK(match != NULL, return NULL);
422
423
0
    if (xpathObj->nodesetval->nodeTab[index]->type != XML_NAMESPACE_DECL) {
424
        // See the comment for pcmk__xpath_result()
425
0
        xpathObj->nodesetval->nodeTab[index] = NULL;
426
0
    }
427
428
0
    switch (match->type) {
429
0
        case XML_ELEMENT_NODE:
430
0
            return match;
431
432
0
        case XML_DOCUMENT_NODE: // Searched for '/'
433
0
            return match->children;
434
435
0
        default:
436
0
           if ((match->parent != NULL)
437
0
               && (match->parent->type == XML_ELEMENT_NODE)) {
438
0
                return match->parent;
439
0
           }
440
0
           pcmk__warn("Unsupported XPath match type %d (bug?)", match->type);
441
0
           return NULL;
442
0
    }
443
0
}
444
445
void
446
freeXpathObject(xmlXPathObjectPtr xpathObj)
447
0
{
448
0
    int max = pcmk__xpath_num_results(xpathObj);
449
450
0
    if (xpathObj == NULL) {
451
0
        return;
452
0
    }
453
454
0
    for (int lpc = 0; lpc < max; lpc++) {
455
0
        if (xpathObj->nodesetval->nodeTab[lpc] && xpathObj->nodesetval->nodeTab[lpc]->type != XML_NAMESPACE_DECL) {
456
0
            xpathObj->nodesetval->nodeTab[lpc] = NULL;
457
0
        }
458
0
    }
459
460
    /* _Now_ it's safe to free it */
461
0
    xmlXPathFreeObject(xpathObj);
462
0
}
463
464
void
465
dedupXpathResults(xmlXPathObjectPtr xpathObj)
466
0
{
467
0
    int max = pcmk__xpath_num_results(xpathObj);
468
469
0
    if (xpathObj == NULL) {
470
0
        return;
471
0
    }
472
473
0
    for (int lpc = 0; lpc < max; lpc++) {
474
0
        xmlNode *xml = NULL;
475
0
        gboolean dedup = FALSE;
476
477
0
        if (xpathObj->nodesetval->nodeTab[lpc] == NULL) {
478
0
            continue;
479
0
        }
480
481
0
        xml = xpathObj->nodesetval->nodeTab[lpc]->parent;
482
483
0
        for (; xml; xml = xml->parent) {
484
0
            int lpc2 = 0;
485
486
0
            for (lpc2 = 0; lpc2 < max; lpc2++) {
487
0
                if (xpathObj->nodesetval->nodeTab[lpc2] == xml) {
488
0
                    xpathObj->nodesetval->nodeTab[lpc] = NULL;
489
0
                    dedup = TRUE;
490
0
                    break;
491
0
                }
492
0
            }
493
494
0
            if (dedup) {
495
0
                break;
496
0
            }
497
0
        }
498
0
    }
499
0
}
500
501
void
502
crm_foreach_xpath_result(xmlNode *xml, const char *xpath,
503
                         void (*helper)(xmlNode*, void*), void *user_data)
504
0
{
505
0
    xmlXPathObject *xpathObj = NULL;
506
0
    int nresults = 0;
507
508
0
    CRM_CHECK(xml != NULL, return);
509
510
0
    xpathObj = pcmk__xpath_search(xml->doc, xpath);
511
0
    nresults = pcmk__xpath_num_results(xpathObj);
512
513
0
    for (int i = 0; i < nresults; i++) {
514
0
        xmlNode *result = pcmk__xpath_result(xpathObj, i);
515
516
0
        CRM_LOG_ASSERT(result != NULL);
517
518
0
        if (result != NULL) {
519
0
            result = pcmk__xpath_match_element(result);
520
521
0
            CRM_LOG_ASSERT(result != NULL);
522
523
0
            if (result != NULL) {
524
0
                helper(result, user_data);
525
0
            }
526
0
        }
527
0
    }
528
0
    xmlXPathFreeObject(xpathObj);
529
0
}
530
531
xmlNode *
532
get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level)
533
0
{
534
0
    int max;
535
0
    xmlNode *result = NULL;
536
0
    xmlXPathObject *xpathObj = NULL;
537
0
    char *nodePath = NULL;
538
0
    char *matchNodePath = NULL;
539
540
0
    if (xpath == NULL) {
541
0
        return xml_obj;         /* or return NULL? */
542
0
    }
543
544
0
    xpathObj = pcmk__xpath_search(xml_obj->doc, xpath);
545
0
    nodePath = (char *)xmlGetNodePath(xml_obj);
546
0
    max = pcmk__xpath_num_results(xpathObj);
547
548
0
    if (max == 0) {
549
0
        if (error_level < PCMK__LOG_NEVER) {
550
0
            do_crm_log(error_level, "No match for %s in %s",
551
0
                       xpath, pcmk__s(nodePath, "unknown path"));
552
0
            crm_log_xml_explicit(xml_obj, "Unexpected Input");
553
0
        }
554
555
0
    } else if (max > 1) {
556
0
        if (error_level < PCMK__LOG_NEVER) {
557
0
            int lpc = 0;
558
559
0
            do_crm_log(error_level, "Too many matches for %s in %s",
560
0
                       xpath, pcmk__s(nodePath, "unknown path"));
561
562
0
            for (lpc = 0; lpc < max; lpc++) {
563
0
                xmlNode *match = pcmk__xpath_result(xpathObj, lpc);
564
565
0
                CRM_LOG_ASSERT(match != NULL);
566
0
                if (match != NULL) {
567
0
                    match = pcmk__xpath_match_element(match);
568
569
0
                    CRM_LOG_ASSERT(match != NULL);
570
0
                    if (match != NULL) {
571
0
                        matchNodePath = (char *) xmlGetNodePath(match);
572
0
                        do_crm_log(error_level, "%s[%d] = %s",
573
0
                                   xpath, lpc,
574
0
                                   pcmk__s(matchNodePath,
575
0
                                           "unrecognizable match"));
576
0
                        free(matchNodePath);
577
0
                    }
578
0
                }
579
0
            }
580
0
            crm_log_xml_explicit(xml_obj, "Bad Input");
581
0
        }
582
583
0
    } else {
584
0
        result = pcmk__xpath_result(xpathObj, 0);
585
0
        if (result != NULL) {
586
0
            result = pcmk__xpath_match_element(result);
587
0
        }
588
0
    }
589
590
0
    xmlXPathFreeObject(xpathObj);
591
0
    free(nodePath);
592
593
0
    return result;
594
0
}
595
596
// LCOV_EXCL_STOP
597
// End deprecated API