diff --git a/lib/common/cib_secrets.c b/lib/common/cib_secrets.c index 455c365d04..ad9010ebf5 100644 --- a/lib/common/cib_secrets.c +++ b/lib/common/cib_secrets.c @@ -1,195 +1,195 @@ /* * Copyright 2011-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 #include #include #include #include #include /*! * \internal * \brief Read file contents into a string, with trailing whitespace removed * * \param[in] filename Name of file to read * * \return File contents as a string, or \c NULL on failure or empty file * * \note It would be simpler to call \c pcmk__file_contents() directly without * trimming trailing whitespace. However, this would change hashes for * existing value files that have trailing whitespace. Similarly, if we * trim trailing whitespace, it would make sense to trim leading * whitespace, but this changes existing hashes. */ static char * read_file_trimmed(const char *filename) { char *p = NULL; char *buf = NULL; int rc = pcmk__file_contents(filename, &buf); if (rc != pcmk_rc_ok) { crm_err("Failed to read %s: %s", filename, pcmk_rc_str(rc)); free(buf); return NULL; } if (buf == NULL) { crm_err("File %s is empty", filename); return NULL; } // Strip trailing white space for (p = buf + strlen(buf) - 1; (p >= buf) && isspace(*p); p--); *(p + 1) = '\0'; return buf; } /*! * \internal * \brief Read checksum from a file and compare against calculated checksum * * \param[in] filename File containing stored checksum * \param[in] secret_value String to calculate checksum from * \param[in] rsc_id Resource ID (for logging only) * \param[in] param Parameter name (for logging only) * * \return Standard Pacemaker return code */ static int validate_hash(const char *filename, const char *secret_value, const char *rsc_id, const char *param) { char *stored = NULL; - char *calculated = NULL; + gchar *calculated = NULL; int rc = pcmk_rc_ok; stored = read_file_trimmed(filename); if (stored == NULL) { crm_err("Could not read md5 sum for resource %s parameter '%s' from " "file '%s'", rsc_id, param, filename); rc = ENOENT; goto done; } - calculated = crm_md5sum(secret_value); + calculated = pcmk__md5sum(secret_value); if (calculated == NULL) { // Should be impossible rc = EINVAL; goto done; } crm_trace("Stored hash: %s, calculated hash: %s", stored, calculated); if (!pcmk__str_eq(stored, calculated, pcmk__str_casei)) { crm_err("Calculated md5 sum for resource %s parameter '%s' does not " "match stored md5 sum", rsc_id, param); rc = pcmk_rc_cib_corrupt; } done: free(stored); - free(calculated); + g_free(calculated); return rc; } /*! * \internal * \brief Read secret parameter values from file * * Given a table of resource parameters, if any of their values are the * magic string indicating a CIB secret, replace that string with the * secret read from the file appropriate to the given resource. * * \param[in] rsc_id Resource whose parameters are being checked * \param[in,out] params Resource parameters to check * * \return Standard Pacemaker return code */ int pcmk__substitute_secrets(const char *rsc_id, GHashTable *params) { GHashTableIter iter; char *param = NULL; char *value = NULL; GString *filename = NULL; gsize dir_len = 0; int rc = pcmk_rc_ok; if (params == NULL) { return pcmk_rc_ok; } // Some params are sent with operations, so we cannot cache secret params g_hash_table_iter_init(&iter, params); while (g_hash_table_iter_next(&iter, (gpointer *) ¶m, (gpointer *) &value)) { char *secret_value = NULL; int hash_rc = pcmk_rc_ok; if (!pcmk__str_eq(value, "lrm://", pcmk__str_none)) { // Not a secret parameter continue; } if (filename == NULL) { // First secret parameter. Fill in directory path for use with all. crm_debug("Replacing secret parameters for resource %s", rsc_id); filename = g_string_sized_new(128); pcmk__g_strcat(filename, PCMK__CIB_SECRETS_DIR "/", rsc_id, "/", NULL); dir_len = filename->len; } else { // Reset filename to the resource's secrets directory path g_string_truncate(filename, dir_len); } // Path to file containing secret value for this parameter g_string_append(filename, param); secret_value = read_file_trimmed(filename->str); if (secret_value == NULL) { crm_err("Secret value for resource %s parameter '%s' not found in " PCMK__CIB_SECRETS_DIR, rsc_id, param); rc = ENOENT; continue; } // Path to file containing md5 sum for this parameter g_string_append(filename, ".sign"); hash_rc = validate_hash(filename->str, secret_value, rsc_id, param); if (hash_rc != pcmk_rc_ok) { rc = hash_rc; free(secret_value); continue; } g_hash_table_iter_replace(&iter, (gpointer) secret_value); } if (filename != NULL) { g_string_free(filename, TRUE); } return rc; } diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h index 49c6c6eadd..bfd09a45d6 100644 --- a/lib/common/crmcommon_private.h +++ b/lib/common/crmcommon_private.h @@ -1,483 +1,488 @@ /* * Copyright 2018-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__COMMON_CRMCOMMON_PRIVATE__H #define PCMK__COMMON_CRMCOMMON_PRIVATE__H /* This header is for the sole use of libcrmcommon, so that functions can be * declared with G_GNUC_INTERNAL for efficiency. */ #include // uint8_t, uint32_t #include // bool #include // size_t #include // G_GNUC_INTERNAL, G_GNUC_PRINTF, gchar, etc. #include // xmlNode, xmlAttr #include // xmlChar #include // struct qb_ipc_response_header #include // pcmk_ipc_api_t, crm_ipc_t, etc. #include // crm_time_t #include // LOG_NEVER #include // mainloop_io_t #include // pcmk__output_t #include // crm_exit_t #include // pcmk_rule_input_t #include // enum pcmk__xml_flags #ifdef __cplusplus extern "C" { #endif // Decent chunk size for processing large amounts of data #define PCMK__BUFFER_SIZE 4096 #if defined(PCMK__UNIT_TESTING) #undef G_GNUC_INTERNAL #define G_GNUC_INTERNAL #endif /*! * \internal * \brief Information about an XML node that was deleted * * When change tracking is enabled and we delete an XML node using * \c pcmk__xml_free(), we free it and add its path and position to a list in * its document's private data. This allows us to display changes, generate * patchsets, etc. * * Note that this does not happen when deleting an XML attribute using * \c pcmk__xa_remove(). In that case: * * If \c force is \c true, we remove the attribute without any tracking. * * If \c force is \c false, we mark the attribute as deleted but leave it in * place until we commit changes. */ typedef struct pcmk__deleted_xml_s { gchar *path; //!< XPath expression identifying the deleted node int position; //!< Position of the deleted node among its siblings } pcmk__deleted_xml_t; /*! * \internal * \brief Private data for an XML node */ typedef struct xml_node_private_s { uint32_t check; //!< Magic number for checking integrity uint32_t flags; //!< Group of enum pcmk__xml_flags xmlNode *match; //!< Pointer to matching node (defined by caller) } xml_node_private_t; /*! * \internal * \brief Private data for an XML document */ typedef struct xml_doc_private_s { uint32_t check; //!< Magic number for checking integrity uint32_t flags; //!< Group of enum pcmk__xml_flags char *acl_user; //!< User affected by \c acls (for logging) //! ACLs to check requested changes against (list of \c xml_acl_t) GList *acls; //! XML nodes marked as deleted (list of \c pcmk__deleted_xml_t) GList *deleted_objs; } xml_doc_private_t; // XML private data magic numbers #define PCMK__XML_DOC_PRIVATE_MAGIC 0x81726354UL #define PCMK__XML_NODE_PRIVATE_MAGIC 0x54637281UL // XML entity references #define PCMK__XML_ENTITY_AMP "&" #define PCMK__XML_ENTITY_GT ">" #define PCMK__XML_ENTITY_LT "<" #define PCMK__XML_ENTITY_QUOT """ #define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \ (xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_set), #flags_to_set); \ } while (0) #define pcmk__clear_xml_flags(xml_priv, flags_to_clear) do { \ (xml_priv)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_clear), #flags_to_clear); \ } while (0) G_GNUC_INTERNAL const char *pcmk__xml_element_type_text(xmlElementType type); G_GNUC_INTERNAL bool pcmk__xml_reset_node_flags(xmlNode *xml, void *user_data); G_GNUC_INTERNAL void pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags); G_GNUC_INTERNAL void pcmk__xml_new_private_data(xmlNode *xml); G_GNUC_INTERNAL void pcmk__xml_free_private_data(xmlNode *xml); G_GNUC_INTERNAL void pcmk__xml_free_node(xmlNode *xml); G_GNUC_INTERNAL xmlDoc *pcmk__xml_new_doc(void); G_GNUC_INTERNAL int pcmk__xml_position(const xmlNode *xml, enum pcmk__xml_flags ignore_if_set); G_GNUC_INTERNAL bool pcmk__xc_matches(const xmlNode *comment1, const xmlNode *comment2); G_GNUC_INTERNAL void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update); G_GNUC_INTERNAL void pcmk__free_acls(GList *acls); G_GNUC_INTERNAL void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user); G_GNUC_INTERNAL bool pcmk__is_user_in_group(const char *user, const char *group); G_GNUC_INTERNAL void pcmk__apply_acl(xmlNode *xml); G_GNUC_INTERNAL void pcmk__apply_creation_acl(xmlNode *xml, bool check_top); G_GNUC_INTERNAL int pcmk__xa_remove(xmlAttr *attr, bool force); G_GNUC_INTERNAL void pcmk__mark_xml_attr_dirty(xmlAttr *a); G_GNUC_INTERNAL bool pcmk__xa_filterable(const char *name); G_GNUC_INTERNAL void pcmk__log_xmllib_err(void *ctx, const char *fmt, ...) G_GNUC_PRINTF(2, 3); G_GNUC_INTERNAL void pcmk__mark_xml_node_dirty(xmlNode *xml); G_GNUC_INTERNAL bool pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data); G_GNUC_INTERNAL void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer); G_GNUC_INTERNAL int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value); G_GNUC_INTERNAL bool pcmk__xml_is_name_start_char(const char *utf8, int *len); G_GNUC_INTERNAL bool pcmk__xml_is_name_char(const char *utf8, int *len); /* * Date/times */ // For use with pcmk__add_time_from_xml() enum pcmk__time_component { pcmk__time_unknown, pcmk__time_years, pcmk__time_months, pcmk__time_weeks, pcmk__time_days, pcmk__time_hours, pcmk__time_minutes, pcmk__time_seconds, }; G_GNUC_INTERNAL const char *pcmk__time_component_attr(enum pcmk__time_component component); G_GNUC_INTERNAL int pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component, const xmlNode *xml); G_GNUC_INTERNAL void pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source); +/* + * Digests + */ + +char *pcmk__md5sum(const char *input); /* * IPC */ #define PCMK__IPC_VERSION 1 #define PCMK__CONTROLD_API_MAJOR "1" #define PCMK__CONTROLD_API_MINOR "0" // IPC behavior that varies by daemon typedef struct pcmk__ipc_methods_s { /*! * \internal * \brief Allocate any private data needed by daemon IPC * * \param[in,out] api IPC API connection * * \return Standard Pacemaker return code */ int (*new_data)(pcmk_ipc_api_t *api); /*! * \internal * \brief Free any private data used by daemon IPC * * \param[in,out] api_data Data allocated by new_data() method */ void (*free_data)(void *api_data); /*! * \internal * \brief Perform daemon-specific handling after successful connection * * Some daemons require clients to register before sending any other * commands. The controller requires a CRM_OP_HELLO (with no reply), and * the CIB manager, executor, and fencer require a CRM_OP_REGISTER (with a * reply). Ideally this would be consistent across all daemons, but for now * this allows each to do its own authorization. * * \param[in,out] api IPC API connection * * \return Standard Pacemaker return code */ int (*post_connect)(pcmk_ipc_api_t *api); /*! * \internal * \brief Check whether an IPC request results in a reply * * \param[in,out] api IPC API connection * \param[in] request IPC request XML * * \return true if request would result in an IPC reply, false otherwise */ bool (*reply_expected)(pcmk_ipc_api_t *api, const xmlNode *request); /*! * \internal * \brief Perform daemon-specific handling of an IPC message * * \param[in,out] api IPC API connection * \param[in,out] msg Message read from IPC connection * * \return true if more IPC reply messages should be expected */ bool (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg); /*! * \internal * \brief Perform daemon-specific handling of an IPC disconnect * * \param[in,out] api IPC API connection */ void (*post_disconnect)(pcmk_ipc_api_t *api); } pcmk__ipc_methods_t; // Implementation of pcmk_ipc_api_t struct pcmk_ipc_api_s { enum pcmk_ipc_server server; // Daemon this IPC API instance is for enum pcmk_ipc_dispatch dispatch_type; // How replies should be dispatched size_t ipc_size_max; // maximum IPC buffer size crm_ipc_t *ipc; // IPC connection mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC bool free_on_disconnect; // Whether disconnect should free object pcmk_ipc_callback_t cb; // Caller-registered callback (if any) void *user_data; // Caller-registered data (if any) void *api_data; // For daemon-specific use pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon }; typedef struct pcmk__ipc_header_s { struct qb_ipc_response_header qb; uint32_t size_uncompressed; uint32_t size_compressed; uint32_t flags; uint8_t version; } pcmk__ipc_header_t; G_GNUC_INTERNAL int pcmk__send_ipc_request(pcmk_ipc_api_t *api, const xmlNode *request); G_GNUC_INTERNAL void pcmk__call_ipc_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type, crm_exit_t status, void *event_data); G_GNUC_INTERNAL unsigned int pcmk__ipc_buffer_size(unsigned int max); G_GNUC_INTERNAL bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__attrd_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__controld_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__pacemakerd_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__schedulerd_api_methods(void); /* * Logging */ //! XML is newly created #define PCMK__XML_PREFIX_CREATED "++" //! XML has been deleted #define PCMK__XML_PREFIX_DELETED "--" //! XML has been modified #define PCMK__XML_PREFIX_MODIFIED "+ " //! XML has been moved #define PCMK__XML_PREFIX_MOVED "+~" /* * Output */ G_GNUC_INTERNAL int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name, const char *filename, char **argv); G_GNUC_INTERNAL void pcmk__register_option_messages(pcmk__output_t *out); G_GNUC_INTERNAL void pcmk__register_patchset_messages(pcmk__output_t *out); G_GNUC_INTERNAL bool pcmk__output_text_get_fancy(pcmk__output_t *out); /* * Rules */ // How node attribute values may be compared in rules enum pcmk__comparison { pcmk__comparison_unknown, pcmk__comparison_defined, pcmk__comparison_undefined, pcmk__comparison_eq, pcmk__comparison_ne, pcmk__comparison_lt, pcmk__comparison_lte, pcmk__comparison_gt, pcmk__comparison_gte, }; // How node attribute values may be parsed in rules enum pcmk__type { pcmk__type_unknown, pcmk__type_string, pcmk__type_integer, pcmk__type_number, pcmk__type_version, }; // Where to obtain reference value for a node attribute comparison enum pcmk__reference_source { pcmk__source_unknown, pcmk__source_literal, pcmk__source_instance_attrs, pcmk__source_meta_attrs, }; G_GNUC_INTERNAL enum pcmk__comparison pcmk__parse_comparison(const char *op); G_GNUC_INTERNAL enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op, const char *value1, const char *value2); G_GNUC_INTERNAL enum pcmk__reference_source pcmk__parse_source(const char *source); G_GNUC_INTERNAL int pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type); G_GNUC_INTERNAL int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start, crm_time_t **end); G_GNUC_INTERNAL int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now); G_GNUC_INTERNAL int pcmk__evaluate_attr_expression(const xmlNode *expression, const pcmk_rule_input_t *rule_input); G_GNUC_INTERNAL int pcmk__evaluate_rsc_expression(const xmlNode *expr, const pcmk_rule_input_t *rule_input); G_GNUC_INTERNAL int pcmk__evaluate_op_expression(const xmlNode *expr, const pcmk_rule_input_t *rule_input); /* * Utils */ #define PCMK__PW_BUFFER_LEN 500 /* * Schemas */ typedef struct { unsigned char v[2]; } pcmk__schema_version_t; enum pcmk__schema_validator { pcmk__schema_validator_none, pcmk__schema_validator_rng }; typedef struct { int schema_index; char *name; /*! * List of XSLT stylesheets for upgrading from this schema version to the * next one. Sorted by the order in which they should be applied to the CIB. */ GList *transforms; void *cache; enum pcmk__schema_validator validator; pcmk__schema_version_t version; } pcmk__schema_t; G_GNUC_INTERNAL GList *pcmk__find_x_0_schema(void); #ifdef __cplusplus } #endif #endif // PCMK__COMMON_CRMCOMMON_PRIVATE__H diff --git a/lib/common/digest.c b/lib/common/digest.c index 56d1ad1a5d..265a32cfb3 100644 --- a/lib/common/digest.c +++ b/lib/common/digest.c @@ -1,390 +1,436 @@ /* * 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 = crm_md5sum((const char *) buffer->str); - crm_log_xml_trace(input, "digest:source"); + 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. */ - char *digest = NULL; 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 = crm_md5sum(buf->str); - if (digest == NULL) { + 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 #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); } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/tests/digest/Makefile.am b/lib/common/tests/digest/Makefile.am index 223a5053d3..1210b22e07 100644 --- a/lib/common/tests/digest/Makefile.am +++ b/lib/common/tests/digest/Makefile.am @@ -1,17 +1,17 @@ # -# Copyright 2024 the Pacemaker project contributors +# Copyright 2024-2025 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # include $(top_srcdir)/mk/common.mk include $(top_srcdir)/mk/tap.mk include $(top_srcdir)/mk/unittest.mk # Add "_test" to the end of all test program names to simplify .gitignore. -check_PROGRAMS = crm_md5sum_test +check_PROGRAMS = pcmk__md5sum_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/digest/crm_md5sum_test.c b/lib/common/tests/digest/pcmk__md5sum_test.c similarity index 70% rename from lib/common/tests/digest/crm_md5sum_test.c rename to lib/common/tests/digest/pcmk__md5sum_test.c index ece4ced2c3..c9f6db1bae 100644 --- a/lib/common/tests/digest/crm_md5sum_test.c +++ b/lib/common/tests/digest/pcmk__md5sum_test.c @@ -1,31 +1,33 @@ /* - * Copyright 2024 the Pacemaker project contributors + * Copyright 2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include +#include "crmcommon_private.h" // pcmk__md5sum() + static void null_arg_test(void **state) { - assert_null(crm_md5sum(NULL)); + assert_null(pcmk__md5sum(NULL)); } static void basic_usage_test(void **state) { - char *result = crm_md5sum("abcdefghijklmnopqrstuvwxyz"); + gchar *result = pcmk__md5sum("abcdefghijklmnopqrstuvwxyz"); assert_string_equal(result, "c3fcd3d76192e4007dfb496cca67e13b"); - free(result); + g_free(result); } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(null_arg_test), cmocka_unit_test(basic_usage_test))