diff --git a/include/crm/common/util.h b/include/crm/common/util.h index 37a5b8843d..b9fa18e598 100644 --- a/include/crm/common/util.h +++ b/include/crm/common/util.h @@ -1,61 +1,60 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_UTIL__H #define PCMK__CRM_COMMON_UTIL__H #include // gid_t, mode_t, size_t, time_t, uid_t #include #include #include // uint32_t #include #include #include #include #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Utility functions * \ingroup core */ /* public node attribute functions (from attrs.c) */ char *pcmk_promotion_score_name(const char *rsc_id); /* public Pacemaker Remote functions (from remote.c) */ int crm_default_remote_port(void); int compare_version(const char *version1, const char *version2); void pcmk_common_cleanup(void); -char *crm_md5sum(const char *buffer); char *crm_generate_uuid(void); int crm_user_lookup(const char *name, uid_t * uid, gid_t * gid); int pcmk_daemon_user(uid_t *uid, gid_t *gid); #ifdef __cplusplus } #endif #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) #include #endif #endif diff --git a/include/crm/common/util_compat.h b/include/crm/common/util_compat.h index f2994258ed..e7b0da6716 100644 --- a/include/crm/common/util_compat.h +++ b/include/crm/common/util_compat.h @@ -1,66 +1,69 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_UTIL_COMPAT__H #define PCMK__CRM_COMMON_UTIL_COMPAT__H #include // bool #include // uint64_t #include // gboolean #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Deprecated Pacemaker utilities * \ingroup core * \deprecated Do not include this header directly. The utilities in this * header, and the header itself, will be removed in a future * release. */ //! \deprecated Use gnutls_global_init() instead void crm_gnutls_global_init(void); //! \deprecated Do not use (will be dropped in a future release) bool crm_is_daemon_name(const char *name); // NOTE: sbd (as of at least 1.5.2) uses this //! \deprecated Do not use static inline gboolean is_set(long long word, long long bit) { return ((word & bit) == bit); } //! \deprecated Do not use static inline bool pcmk_any_flags_set(uint64_t flag_group, uint64_t flags_to_check) { return (flag_group & flags_to_check) != 0; } //! \deprecated Do not use static inline bool pcmk_all_flags_set(uint64_t flag_group, uint64_t flags_to_check) { return (flag_group & flags_to_check) == flags_to_check; } //! \deprecated Do not use #define pcmk_is_set(g, f) pcmk_all_flags_set((g), (f)) +//! \deprecated Do not use +char *crm_md5sum(const char *buffer); + #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_UTIL_COMPAT__H diff --git a/lib/common/digest.c b/lib/common/digest.c index 265a32cfb3..f54e5b8337 100644 --- a/lib/common/digest.c +++ b/lib/common/digest.c @@ -1,436 +1,437 @@ /* * Copyright 2015-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include // GString, etc. #include // gnutls_hash_fast(), gnutls_hash_get_len() #include // gnutls_strerror() #include #include #include "crmcommon_private.h" #define BEST_EFFORT_STATUS 0 /* * Pacemaker uses digests (MD5 hashes) of stringified XML to detect changes in * the CIB as a whole, a particular resource's agent parameters, and the device * parameters last used to unfence a particular node. * * "v2" digests hash pcmk__xml_string() directly, while less efficient "v1" * digests do the same with a prefixed space, suffixed newline, and optional * pre-sorting. * * On-disk CIB digests use v1 without sorting. * * Operation digests use v1 with sorting, and are stored in a resource's * operation history in the CIB status section. They come in three flavors: * - a digest of (nearly) all resource parameters and options, used to detect * any resource configuration change; * - a digest of resource parameters marked as nonreloadable, used to decide * whether a reload or full restart is needed after a configuration change; * - and a digest of resource parameters not marked as private, used in * simulations where private parameters have been removed from the input. * * Unfencing digests are set as node attributes, and are used to require * that nodes be unfenced again after a device's configuration changes. */ /*! * \internal * \brief Dump XML in a format used with v1 digests * * \param[in] xml Root of XML to dump * * \return Newly allocated buffer containing dumped XML */ static GString * dump_xml_for_digest(const xmlNode *xml) { GString *buffer = g_string_sized_new(1024); /* for compatibility with the old result which is used for v1 digests */ g_string_append_c(buffer, ' '); pcmk__xml_string(xml, 0, buffer, 0); g_string_append_c(buffer, '\n'); return buffer; } /*! * \internal * \brief Compute an MD5 checksum for a given input string * * \param[in] input Input string (can be \c NULL) * * \return Newly allocated string containing MD5 checksum for \p input, or * \c NULL on error or if \p input is \c NULL * * \note The caller is responsible for freeing the return value using \c free(). */ char * pcmk__md5sum(const char *input) { /* This function makes two copies of the same string: one inside * g_compute_checksum_for_string() and one from pcmk__str_copy(). The first * one gets freed before return. * * We could avoid this by calling g_checksum_new(), g_checksum_get_string() * (copying the return value with pcmk__str_copy(), and g_checksum_free() * directly; or by updating our own call chains to use (gchar *) strings. * However, the below is much more readable. */ char *checksum = NULL; gchar *checksum_g = NULL; if (input == NULL) { return NULL; } checksum_g = g_compute_checksum_for_string(G_CHECKSUM_MD5, input, -1); if (checksum_g == NULL) { crm_err("Failed to compute MD5 checksum for %s", input); } checksum = pcmk__str_copy(checksum_g); g_free(checksum_g); return checksum; } /*! * \internal * \brief Calculate and return v1 digest of XML tree * * \param[in] input Root of XML to digest * * \return Newly allocated string containing digest * * \note Example return value: "c048eae664dba840e1d2060f00299e9d" */ static char * calculate_xml_digest_v1(const xmlNode *input) { GString *buffer = dump_xml_for_digest(input); gchar *digest_g = NULL; char *digest = NULL; // buffer->len > 2 for initial space and trailing newline CRM_CHECK(buffer->len > 2, g_string_free(buffer, TRUE); return NULL); digest_g = pcmk__md5sum(buffer->str); digest = pcmk__str_copy(digest_g); g_string_free(buffer, TRUE); g_free(digest_g); return digest; } /*! * \internal * \brief Calculate and return the digest of a CIB, suitable for storing on disk * * \param[in] input Root of XML to digest * * \return Newly allocated string containing digest */ char * pcmk__digest_on_disk_cib(const xmlNode *input) { /* Always use the v1 format for on-disk digests. * * Switching to v2 affects even full-restart upgrades, so it would be a * compatibility nightmare. * * We only use this once at startup. All other invocations are in a * separate child process. */ return calculate_xml_digest_v1(input); } /*! * \internal * \brief Calculate and return digest of an operation XML element * * The digest is invariant to changes in the order of XML attributes. * * \param[in] input Root of XML to digest (must have no children) * * \return Newly allocated string containing digest */ char * pcmk__digest_operation(const xmlNode *input) { /* Switching to v2 digests would likely cause restarts during rolling * upgrades. * * @TODO Confirm this. Switch to v2 if safe, or drop this TODO otherwise. */ char *digest = NULL; xmlNode *sorted = NULL; pcmk__assert(input->children == NULL); sorted = pcmk__xe_create(NULL, (const char *) input->name); pcmk__xe_copy_attrs(sorted, input, pcmk__xaf_none); pcmk__xe_sort_attrs(sorted); digest = calculate_xml_digest_v1(sorted); pcmk__xml_free(sorted); return digest; } /*! * \internal * \brief Calculate and return the digest of an XML tree * * \param[in] xml XML tree to digest * \param[in] filter Whether to filter certain XML attributes * * \return Newly allocated string containing digest */ char * pcmk__digest_xml(const xmlNode *xml, bool filter) { /* @TODO Filtering accounts for significant CPU usage. Consider removing if * possible. */ GString *buf = g_string_sized_new(1024); gchar *digest_g = NULL; char *digest = NULL; pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0); digest_g = pcmk__md5sum(buf->str); if (digest_g == NULL) { goto done; } digest = pcmk__str_copy(digest_g); pcmk__if_tracing( { char *trace_file = pcmk__assert_asprintf("digest-%s", digest); crm_trace("Saving %s.%s.%s to %s", pcmk__xe_get(xml, PCMK_XA_ADMIN_EPOCH), pcmk__xe_get(xml, PCMK_XA_EPOCH), pcmk__xe_get(xml, PCMK_XA_NUM_UPDATES), trace_file); pcmk__xml_write_temp_file(xml, "digest input", trace_file); free(trace_file); }, {} ); done: g_string_free(buf, TRUE); g_free(digest_g); return digest; } /*! * \internal * \brief Check whether calculated digest of given XML matches expected digest * * \param[in] input Root of XML tree to digest * \param[in] expected Expected digest in on-disk format * * \return true if digests match, false on mismatch or error */ bool pcmk__verify_digest(const xmlNode *input, const char *expected) { char *calculated = NULL; bool passed; if (input != NULL) { calculated = pcmk__digest_on_disk_cib(input); if (calculated == NULL) { crm_perror(LOG_ERR, "Could not calculate digest for comparison"); return false; } } passed = pcmk__str_eq(expected, calculated, pcmk__str_casei); if (passed) { crm_trace("Digest comparison passed: %s", calculated); } else { crm_err("Digest comparison failed: expected %s, calculated %s", expected, calculated); } free(calculated); return passed; } /*! * \internal * \brief Check whether an XML attribute should be excluded from CIB digests * * \param[in] name XML attribute name * * \return true if XML attribute should be excluded from CIB digest calculation */ bool pcmk__xa_filterable(const char *name) { static const char *filter[] = { PCMK_XA_CRM_DEBUG_ORIGIN, PCMK_XA_CIB_LAST_WRITTEN, PCMK_XA_UPDATE_ORIGIN, PCMK_XA_UPDATE_CLIENT, PCMK_XA_UPDATE_USER, }; for (int i = 0; i < PCMK__NELEM(filter); i++) { if (strcmp(name, filter[i]) == 0) { return true; } } return false; } -char * -crm_md5sum(const char *buffer) -{ - char *digest = NULL; - gchar *raw_digest = NULL; - - if (buffer == NULL) { - return NULL; - } - - raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1); - - if (raw_digest == NULL) { - crm_err("Failed to calculate hash"); - return NULL; - } - - digest = pcmk__str_copy(raw_digest); - g_free(raw_digest); - - crm_trace("Digest %s.", digest); - return digest; -} - // Return true if a is an attribute that should be filtered static bool should_filter_for_digest(xmlAttrPtr a, void *user_data) { if (strncmp((const char *) a->name, CRM_META "_", sizeof(CRM_META " ") - 1) == 0) { return true; } return pcmk__str_any_of((const char *) a->name, PCMK_XA_ID, PCMK_XA_CRM_FEATURE_SET, PCMK__XA_OP_DIGEST, PCMK__META_ON_NODE, PCMK__META_ON_NODE_UUID, "pcmk_external_ip", NULL); } /*! * \internal * \brief Remove XML attributes not needed for operation digest * * \param[in,out] param_set XML with operation parameters */ void pcmk__filter_op_for_digest(xmlNode *param_set) { char *key = NULL; char *timeout = NULL; guint interval_ms = 0; if (param_set == NULL) { return; } /* Timeout is useful for recurring operation digests, so grab it before * removing meta-attributes */ key = crm_meta_name(PCMK_META_INTERVAL); pcmk__xe_get_guint(param_set, key, &interval_ms); free(key); key = NULL; if (interval_ms != 0) { key = crm_meta_name(PCMK_META_TIMEOUT); timeout = pcmk__xe_get_copy(param_set, key); } // Remove all CRM_meta_* attributes and certain other attributes pcmk__xe_remove_matching_attrs(param_set, false, should_filter_for_digest, NULL); // Add timeout back for recurring operation digests if (timeout != NULL) { pcmk__xe_set(param_set, key, timeout); } free(timeout); free(key); } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START +#include // crm_md5sum() #include #include char * calculate_on_disk_digest(xmlNode *input) { return calculate_xml_digest_v1(input); } char * calculate_operation_digest(xmlNode *input, const char *version) { xmlNode *sorted = sorted_xml(input, NULL, true); char *digest = calculate_xml_digest_v1(sorted); pcmk__xml_free(sorted); return digest; } char * calculate_xml_versioned_digest(xmlNode *input, gboolean sort, gboolean do_filter, const char *version) { if ((version == NULL) || (compare_version("3.0.5", version) > 0)) { xmlNode *sorted = NULL; char *digest = NULL; if (sort) { xmlNode *sorted = sorted_xml(input, NULL, true); input = sorted; } crm_trace("Using v1 digest algorithm for %s", pcmk__s(version, "unknown feature set")); digest = calculate_xml_digest_v1(input); pcmk__xml_free(sorted); return digest; } crm_trace("Using v2 digest algorithm for %s", version); return pcmk__digest_xml(input, do_filter); } +char * +crm_md5sum(const char *buffer) +{ + char *digest = NULL; + gchar *raw_digest = NULL; + + if (buffer == NULL) { + return NULL; + } + + raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1); + + if (raw_digest == NULL) { + crm_err("Failed to calculate hash"); + return NULL; + } + + digest = pcmk__str_copy(raw_digest); + g_free(raw_digest); + + crm_trace("Digest %s.", digest); + return digest; +} + // LCOV_EXCL_STOP // End deprecated API