Coverage Report

Created: 2025-12-31 06:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pacemaker/lib/common/digest.c
Line
Count
Source
1
/*
2
 * Copyright 2015-2025 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
12
#include <stdbool.h>
13
#include <stdio.h>
14
#include <unistd.h>
15
#include <string.h>
16
#include <stdlib.h>
17
18
#include <glib.h>               // GString, etc.
19
#include <gnutls/crypto.h>      // gnutls_hash_fast(), gnutls_hash_get_len()
20
#include <gnutls/gnutls.h>      // gnutls_strerror()
21
22
#include <crm/crm.h>
23
#include <crm/common/xml.h>
24
#include "crmcommon_private.h"
25
26
#define BEST_EFFORT_STATUS 0
27
28
/*
29
 * Pacemaker uses digests (MD5 hashes) of stringified XML to detect changes in
30
 * the CIB as a whole, a particular resource's agent parameters, and the device
31
 * parameters last used to unfence a particular node.
32
 *
33
 * "v2" digests hash pcmk__xml_string() directly, while less efficient "v1"
34
 * digests do the same with a prefixed space, suffixed newline, and optional
35
 * pre-sorting.
36
 *
37
 * On-disk CIB digests use v1 without sorting.
38
 *
39
 * Operation digests use v1 with sorting, and are stored in a resource's
40
 * operation history in the CIB status section. They come in three flavors:
41
 * - a digest of (nearly) all resource parameters and options, used to detect
42
 *   any resource configuration change;
43
 * - a digest of resource parameters marked as nonreloadable, used to decide
44
 *   whether a reload or full restart is needed after a configuration change;
45
 * - and a digest of resource parameters not marked as private, used in
46
 *   simulations where private parameters have been removed from the input.
47
 *
48
 * Unfencing digests are set as node attributes, and are used to require
49
 * that nodes be unfenced again after a device's configuration changes.
50
 */
51
52
/*!
53
 * \internal
54
 * \brief Dump XML in a format used with v1 digests
55
 *
56
 * \param[in] xml  Root of XML to dump
57
 *
58
 * \return Newly allocated buffer containing dumped XML
59
 */
60
static GString *
61
dump_xml_for_digest(const xmlNode *xml)
62
0
{
63
0
    GString *buffer = g_string_sized_new(1024);
64
65
    /* for compatibility with the old result which is used for v1 digests */
66
0
    g_string_append_c(buffer, ' ');
67
0
    pcmk__xml_string(xml, 0, buffer, 0);
68
0
    g_string_append_c(buffer, '\n');
69
70
0
    return buffer;
71
0
}
72
73
/*!
74
 * \internal
75
 * \brief Compute an MD5 checksum for a given input string
76
 *
77
 * \param[in] input  Input string (can be \c NULL)
78
 *
79
 * \return Newly allocated string containing MD5 checksum for \p input, or
80
 *         \c NULL on error or if \p input is \c NULL
81
 *
82
 * \note The caller is responsible for freeing the return value using \c free().
83
 */
84
char *
85
pcmk__md5sum(const char *input)
86
0
{
87
0
    char *checksum = NULL;
88
0
    gchar *checksum_g = NULL;
89
90
0
    if (input == NULL) {
91
0
        return NULL;
92
0
    }
93
94
    /* g_compute_checksum_for_string() returns NULL if the input string is
95
     * empty. There are instances where we may want to hash an empty, but
96
     * non-NULL, string, so here we just hardcode the result.
97
     */
98
0
    if (pcmk__str_empty(input)) {
99
0
        return pcmk__str_copy("d41d8cd98f00b204e9800998ecf8427e");
100
0
    }
101
102
0
    checksum_g = g_compute_checksum_for_string(G_CHECKSUM_MD5, input, -1);
103
0
    if (checksum_g == NULL) {
104
0
        pcmk__err("Failed to compute MD5 checksum for %s", input);
105
0
        return NULL;
106
0
    }
107
108
    // Make a copy just so that callers can use free() instead of g_free()
109
0
    checksum = pcmk__str_copy(checksum_g);
110
0
    g_free(checksum_g);
111
0
    return checksum;
112
0
}
113
114
/*!
115
 * \internal
116
 * \brief Calculate and return v1 digest of XML tree
117
 *
118
 * \param[in] input  Root of XML to digest
119
 *
120
 * \return Newly allocated string containing digest
121
 *
122
 * \note Example return value: "c048eae664dba840e1d2060f00299e9d"
123
 */
124
static char *
125
calculate_xml_digest_v1(const xmlNode *input)
126
0
{
127
0
    GString *buffer = dump_xml_for_digest(input);
128
0
    char *digest = NULL;
129
130
    // buffer->len > 2 for initial space and trailing newline
131
0
    CRM_CHECK(buffer->len > 2,
132
0
              g_string_free(buffer, TRUE);
133
0
              return NULL);
134
135
0
    digest = pcmk__md5sum(buffer->str);
136
137
0
    g_string_free(buffer, TRUE);
138
0
    return digest;
139
0
}
140
141
/*!
142
 * \internal
143
 * \brief Calculate and return the digest of a CIB, suitable for storing on disk
144
 *
145
 * \param[in] input  Root of XML to digest
146
 *
147
 * \return Newly allocated string containing digest
148
 */
149
char *
150
pcmk__digest_on_disk_cib(const xmlNode *input)
151
0
{
152
    /* Always use the v1 format for on-disk digests.
153
     * * Switching to v2 affects even full-restart upgrades, so it would be a
154
     *   compatibility nightmare.
155
     * * We only use this once at startup. All other invocations are in a
156
     *   separate child process.
157
     */
158
0
    return calculate_xml_digest_v1(input);
159
0
}
160
161
/*!
162
 * \internal
163
 * \brief Calculate and return digest of a \c PCMK_XE_PARAMETERS element
164
 *
165
 * This is intended for parameters of a resource operation (also known as
166
 * resource action). A \c PCMK_XE_PARAMETERS element from a different source
167
 * (for example, resource agent metadata) may have child elements, which are not
168
 * allowed here.
169
 *
170
 * The digest is invariant to changes in the order of XML attributes.
171
 *
172
 * \param[in] input  XML element to digest (must have no children)
173
 *
174
 * \return Newly allocated string containing digest
175
 */
176
char *
177
pcmk__digest_op_params(const xmlNode *input)
178
0
{
179
    /* Switching to v2 digests would likely cause restarts during rolling
180
     * upgrades.
181
     *
182
     * @TODO Confirm this. Switch to v2 if safe, or drop this TODO otherwise.
183
     */
184
0
    char *digest = NULL;
185
0
    xmlNode *sorted = NULL;
186
187
0
    pcmk__assert(input->children == NULL);
188
189
0
    sorted = pcmk__xe_create(NULL, (const char *) input->name);
190
0
    pcmk__xe_copy_attrs(sorted, input, pcmk__xaf_none);
191
0
    pcmk__xe_sort_attrs(sorted);
192
193
0
    digest = calculate_xml_digest_v1(sorted);
194
195
0
    pcmk__xml_free(sorted);
196
0
    return digest;
197
0
}
198
199
/*!
200
 * \internal
201
 * \brief Calculate and return the digest of an XML tree
202
 *
203
 * \param[in] xml     XML tree to digest
204
 * \param[in] filter  Whether to filter certain XML attributes
205
 *
206
 * \return Newly allocated string containing digest
207
 */
208
char *
209
pcmk__digest_xml(const xmlNode *xml, bool filter)
210
0
{
211
    /* @TODO Filtering accounts for significant CPU usage. Consider removing if
212
     * possible.
213
     */
214
0
    char *digest = NULL;
215
0
    GString *buf = g_string_sized_new(1024);
216
217
0
    pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0);
218
0
    digest = pcmk__md5sum(buf->str);
219
0
    if (digest == NULL) {
220
0
        goto done;
221
0
    }
222
223
0
    pcmk__if_tracing(
224
0
        {
225
0
            char *trace_file = pcmk__assert_asprintf("digest-%s", digest);
226
227
0
            pcmk__trace("Saving %s.%s.%s to %s",
228
0
                        pcmk__xe_get(xml, PCMK_XA_ADMIN_EPOCH),
229
0
                        pcmk__xe_get(xml, PCMK_XA_EPOCH),
230
0
                        pcmk__xe_get(xml, PCMK_XA_NUM_UPDATES), trace_file);
231
0
            pcmk__xml_write_temp_file(xml, "digest input", trace_file);
232
0
            free(trace_file);
233
0
        },
234
0
        {}
235
0
    );
236
237
0
done:
238
0
    g_string_free(buf, TRUE);
239
0
    return digest;
240
0
}
241
242
/*!
243
 * \internal
244
 * \brief Check whether calculated digest of given XML matches expected digest
245
 *
246
 * \param[in] input     Root of XML tree to digest
247
 * \param[in] expected  Expected digest in on-disk format
248
 *
249
 * \return true if digests match, false on mismatch or error
250
 */
251
bool
252
pcmk__verify_digest(const xmlNode *input, const char *expected)
253
0
{
254
0
    char *calculated = NULL;
255
0
    bool passed;
256
257
0
    if (input != NULL) {
258
0
        calculated = pcmk__digest_on_disk_cib(input);
259
0
        if (calculated == NULL) {
260
0
            pcmk__err("Could not calculate digest for comparison");
261
0
            return false;
262
0
        }
263
0
    }
264
0
    passed = pcmk__str_eq(expected, calculated, pcmk__str_casei);
265
0
    if (passed) {
266
0
        pcmk__trace("Digest comparison passed: %s", calculated);
267
0
    } else {
268
0
        pcmk__err("Digest comparison failed: expected %s, calculated %s",
269
0
                  expected, calculated);
270
0
    }
271
0
    free(calculated);
272
0
    return passed;
273
0
}
274
275
/*!
276
 * \internal
277
 * \brief Check whether an XML attribute should be excluded from CIB digests
278
 *
279
 * \param[in] name  XML attribute name
280
 *
281
 * \return true if XML attribute should be excluded from CIB digest calculation
282
 */
283
bool
284
pcmk__xa_filterable(const char *name)
285
0
{
286
0
    static const char *filter[] = {
287
0
        PCMK_XA_CRM_DEBUG_ORIGIN,
288
0
        PCMK_XA_CIB_LAST_WRITTEN,
289
0
        PCMK_XA_UPDATE_ORIGIN,
290
0
        PCMK_XA_UPDATE_CLIENT,
291
0
        PCMK_XA_UPDATE_USER,
292
0
    };
293
294
0
    for (int i = 0; i < PCMK__NELEM(filter); i++) {
295
0
        if (strcmp(name, filter[i]) == 0) {
296
0
            return true;
297
0
        }
298
0
    }
299
0
    return false;
300
0
}
301
302
// Return true if a is an attribute that should be filtered
303
static bool
304
should_filter_for_digest(xmlAttrPtr a, void *user_data)
305
0
{
306
0
    if (strncmp((const char *) a->name, CRM_META "_",
307
0
                sizeof(CRM_META " ") - 1) == 0) {
308
0
        return true;
309
0
    }
310
0
    return pcmk__str_any_of((const char *) a->name,
311
0
                            PCMK_XA_ID,
312
0
                            PCMK_XA_CRM_FEATURE_SET,
313
0
                            PCMK__XA_OP_DIGEST,
314
0
                            PCMK__META_ON_NODE,
315
0
                            PCMK__META_ON_NODE_UUID,
316
0
                            "pcmk_external_ip",
317
0
                            NULL);
318
0
}
319
320
/*!
321
 * \internal
322
 * \brief Remove XML attributes not needed for operation digest
323
 *
324
 * \param[in,out] param_set  XML with operation parameters
325
 */
326
void
327
pcmk__filter_op_for_digest(xmlNode *param_set)
328
0
{
329
0
    char *key = NULL;
330
0
    char *timeout = NULL;
331
0
    guint interval_ms = 0;
332
333
0
    if (param_set == NULL) {
334
0
        return;
335
0
    }
336
337
    /* Timeout is useful for recurring operation digests, so grab it before
338
     * removing meta-attributes
339
     */
340
0
    key = crm_meta_name(PCMK_META_INTERVAL);
341
0
    pcmk__xe_get_guint(param_set, key, &interval_ms);
342
0
    free(key);
343
0
    key = NULL;
344
0
    if (interval_ms != 0) {
345
0
        key = crm_meta_name(PCMK_META_TIMEOUT);
346
0
        timeout = pcmk__xe_get_copy(param_set, key);
347
0
    }
348
349
    // Remove all CRM_meta_* attributes and certain other attributes
350
0
    pcmk__xe_remove_matching_attrs(param_set, false, should_filter_for_digest,
351
0
                                   NULL);
352
353
    // Add timeout back for recurring operation digests
354
0
    if (timeout != NULL) {
355
0
        pcmk__xe_set(param_set, key, timeout);
356
0
    }
357
0
    free(timeout);
358
0
    free(key);
359
0
}
360
361
// Deprecated functions kept only for backward API compatibility
362
// LCOV_EXCL_START
363
364
#include <crm/common/util_compat.h>         // crm_md5sum()
365
#include <crm/common/xml_compat.h>
366
#include <crm/common/xml_element_compat.h>
367
368
char *
369
calculate_on_disk_digest(xmlNode *input)
370
0
{
371
0
    return calculate_xml_digest_v1(input);
372
0
}
373
374
char *
375
calculate_operation_digest(xmlNode *input, const char *version)
376
0
{
377
0
    xmlNode *sorted = sorted_xml(input, NULL, true);
378
0
    char *digest = calculate_xml_digest_v1(sorted);
379
380
0
    pcmk__xml_free(sorted);
381
0
    return digest;
382
0
}
383
384
char *
385
calculate_xml_versioned_digest(xmlNode *input, gboolean sort,
386
                               gboolean do_filter, const char *version)
387
0
{
388
0
    if ((version == NULL) || (pcmk__compare_versions("3.0.5", version) > 0)) {
389
0
        xmlNode *sorted = NULL;
390
0
        char *digest = NULL;
391
392
0
        if (sort) {
393
0
            xmlNode *sorted = sorted_xml(input, NULL, true);
394
395
0
            input = sorted;
396
0
        }
397
398
0
        pcmk__trace("Using v1 digest algorithm for %s",
399
0
                    pcmk__s(version, "unknown feature set"));
400
401
0
        digest = calculate_xml_digest_v1(input);
402
403
0
        pcmk__xml_free(sorted);
404
0
        return digest;
405
0
    }
406
0
    pcmk__trace("Using v2 digest algorithm for %s", version);
407
0
    return pcmk__digest_xml(input, do_filter);
408
0
}
409
410
char *
411
crm_md5sum(const char *buffer)
412
0
{
413
0
    char *digest = NULL;
414
0
    gchar *raw_digest = NULL;
415
416
    /* g_compute_checksum_for_string returns NULL if the input string is empty.
417
     * There are instances where we may want to hash an empty, but non-NULL,
418
     * string so here we just hardcode the result.
419
     */
420
0
    if (buffer == NULL) {
421
0
        return NULL;
422
0
    } else if (pcmk__str_empty(buffer)) {
423
0
        return pcmk__str_copy("d41d8cd98f00b204e9800998ecf8427e");
424
0
    }
425
426
0
    raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1);
427
428
0
    if (raw_digest == NULL) {
429
0
        pcmk__err("Failed to calculate hash");
430
0
        return NULL;
431
0
    }
432
433
0
    digest = pcmk__str_copy(raw_digest);
434
0
    g_free(raw_digest);
435
436
0
    pcmk__trace("Digest %s.", digest);
437
0
    return digest;
438
0
}
439
440
// LCOV_EXCL_STOP
441
// End deprecated API