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