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