diff --git a/cts/valgrind-pcmk.suppressions b/cts/valgrind-pcmk.suppressions
index 461edc250b..a470d7598c 100644
--- a/cts/valgrind-pcmk.suppressions
+++ b/cts/valgrind-pcmk.suppressions
@@ -1,295 +1,351 @@
 # Valgrind suppressions for Pacemaker testing
 {
    Valgrind bug
    Memcheck:Addr8
    fun:__strspn_sse42
    fun:crm_get_msec
 }
 
 {
    dlopen internals
    Memcheck:Leak
    fun:calloc
    fun:_dlerror_run
    fun:dlopen*
    fun:_log_so_walk_callback
    fun:dl_iterate_phdr
    fun:qb_log_init
 }
 
 # Numerous leaks in bash
 
 {
    Bash reader_loop leaks
    Memcheck:Leak
    fun:malloc
    fun:xmalloc
    ...
    fun:reader_loop
    fun:main
 }
 
 {
    Bash set_default_locale leaks
    Memcheck:Leak
    fun:malloc
    fun:xmalloc
    fun:set_default_locale
    fun:main
 }
 
 {
    Bash execute_command leaks
    Memcheck:Leak
    fun:malloc
    fun:xmalloc
    obj:*/bash
    ...
    fun:execute_command_internal
    fun:execute_command
    ...
 }
 
 # Numerous leaks in glib
 
 {
    quarks - hashtable
    Memcheck:Leak
    fun:calloc
    fun:g_malloc0
    obj:*/libglib-*
    fun:g_slice_alloc
    fun:g_hash_table_new_full
    fun:g_quark_from_static_string
 }
 
 {
    quarks - hashtable 2
    Memcheck:Leak
    fun:malloc
    fun:g_malloc
    fun:g_slice_alloc
    fun:g_hash_table_new_full
    fun:g_quark_from_static_string
 }
 
 {
    quarks - hashtable 3
    Memcheck:Leak
    fun:calloc
    fun:g_malloc0
    fun:g_hash_table_new_full
    fun:g_quark_from_static_string
 }
 
 {
    quarks - hashtable 4
    Memcheck:Leak
    fun:malloc
    fun:realloc
    fun:g_realloc
    fun:g_quark_from_static_string
 }
 
 {
-   glib mainloop internals - default
+   glib - mainloop new calloc
    Memcheck:Leak
    fun:calloc
    fun:g_malloc0
    fun:g_main_context_new
    fun:g_main_context_default
    fun:g_main_loop_new
+   ...
    fun:main
 }
 
 {
-   glib mainloop internals - default
+   glib - mainloop new malloc
    Memcheck:Leak
    fun:malloc
    fun:g_malloc
-   fun:g_slice_alloc
-   fun:*
+   ...
    fun:g_main_context_new
    fun:g_main_context_default
    fun:g_main_loop_new
+   ...
    fun:main
 }
 
 {
-   glib mainloop internals - default
+   glib - mainloop new calloc 2
    Memcheck:Leak
    fun:calloc
    fun:g_malloc0
    obj:*/libglib-2.*
    fun:g_slice_alloc
    fun:g_ptr_array_sized_new
    fun:g_main_context_new
    fun:g_main_context_default
    fun:g_main_loop_new
 }
 
 {
-   glib mainloop internals - run
+   glib - mainloop run calloc
    Memcheck:Leak
    fun:calloc
    fun:g_malloc0
    fun:g_thread_self
    fun:g_main_loop_run
 }
 
 {
-   glib mainloop internals - run
+   glib - mainloop run malloc
    Memcheck:Leak
    fun:malloc
    fun:g_malloc
    obj:*/libglib-2.*
    fun:g_main_loop_run
 }
 
 {
-   glib mainloop internals - run
+   glib - mainloop run malloc 2
    Memcheck:Leak
    fun:malloc
    fun:realloc
    fun:g_realloc
    obj:*/libglib-2.*
    fun:g_ptr_array_add
    fun:g_main_context_check
    obj:*/libglib-2.*
    fun:g_main_loop_run
 }
 
 {
-   glib mainloop internals - run
+   glib - mainloop run malloc 3
    Memcheck:Leak
    fun:malloc
-   fun:g_malloc
-   fun:g_slice_alloc
-   fun:g_slice_alloc0
+   fun:realloc
+   fun:g_realloc
+   obj:*/libglib-2.*
+   fun:g_array_set_size
+   fun:g_static_private_set
    obj:*/libglib-2.*
    fun:g_main_context_dispatch
    obj:*/libglib-2.*
    fun:g_main_loop_run
 }
 
 {
-   glib mainloop internals - run
+   glib - mainloop run malloc 4
+   Memcheck:Leak
+   fun:malloc
+   obj:*/libglib-2.*
+   fun:g_private_get
+   fun:g_thread_self
+   fun:g_main_loop_run
+}
+
+{
+   glib - mainloop run malloc 5
    Memcheck:Leak
    fun:malloc
    fun:realloc
-   fun:g_realloc
    obj:*/libglib-2.*
-   fun:g_array_set_size
-   fun:g_static_private_set
+   fun:g_private_get
+   fun:g_thread_self
+   fun:g_main_loop_run
+}
+
+{
+   glib - mainloop run malloc 6
+   Memcheck:Leak
+   fun:malloc
    obj:*/libglib-2.*
+   fun:g_private_get
    fun:g_main_context_dispatch
    obj:*/libglib-2.*
    fun:g_main_loop_run
 }
 
 {
-   glib mainloop internals - run
+   glib - mainloop run malloc 7
    Memcheck:Leak
    fun:malloc
    fun:g_malloc
    fun:g_slice_alloc
    fun:g_array_sized_new
    fun:g_static_private_set
    obj:*/libglib-2.*
    fun:g_main_context_dispatch
    obj:*/libglib-2.*
    fun:g_main_loop_run
 }
 
 {
-   glib types
+   glib - mainloop run malloc 8
    Memcheck:Leak
+   match-leak-kinds: reachable
    fun:malloc
    fun:realloc
    fun:g_realloc
    obj:*/libgobject-*
    fun:g_type_register_static
 }
 
 {
-   glib types 2
+   glib - register malloc
    Memcheck:Leak
+   fun:malloc
    fun:realloc
    fun:g_realloc
    obj:*/libgobject-*
    fun:g_type_register_static
-   fun:g_param_type_register_static
 }
 
 {
-   glib types 3
+   glib - register realloc
    Memcheck:Leak
-   fun:calloc
-   fun:g_malloc0
+   fun:realloc
+   fun:g_realloc
    obj:*/libgobject-*
-   fun:g_type_register_fundamental
+   fun:g_type_register_static
+   fun:g_param_type_register_static
 }
 
 {
-   glib types 4
+   glib - types register calloc
    Memcheck:Leak
    fun:calloc
    fun:g_malloc0
-   obj:*/libgobject-*
+   ...
    obj:*/libgobject-*
    fun:g_type_register_fundamental
 }
 
 {
-   glib - the return
+   glib - init calloc
    Memcheck:Leak
    fun:calloc
    fun:g_malloc0
-   obj:*/libgobject-*
+   ...
    obj:*/libgobject-*
    fun:_dl_init
 }
 
 {
-   glib - seriously?
+   glib - init calloc 2
    Memcheck:Leak
    fun:calloc
    fun:g_malloc0
+   ...
    obj:*/libgobject-*
-   obj:*/libgobject-*
-   obj:*/libgobject-*
+   fun:call_init*
    fun:_dl_init
 }
 
 {
-   glib - this is not funny anymore
+   glib - register malloc 2
    Memcheck:Leak
    fun:malloc
    fun:realloc
    fun:g_realloc
    obj:*/libgobject-*
    fun:g_type_register_fundamental
 }
 
 {
-   glib - why do you hate me?
+   glib - hashtable new calloc
    Memcheck:Leak
+   match-leak-kinds: reachable
    fun:calloc
    fun:g_malloc0
-   obj:*/libgobject-*
-   obj:*/libgobject-*
-   fun:call_init.part.0
+   ...
+   fun:g_hash_table_new_full
+   ...
+   obj:*/libglib-*
+   ...
+   fun:call_init
    fun:_dl_init
 }
 
 {
-   dear glib - you suck at memory management
+   glib - hashtable new malloc
    Memcheck:Leak
-   fun:calloc
-   fun:g_malloc0
-   obj:*/libgobject-*
-   obj:*/libgobject-*
-   obj:*/libgobject-*
-   fun:call_init.part.0
+   match-leak-kinds: reachable
+   fun:malloc
+   ...
+   fun:g_hash_table_new_full
+   ...
+   obj:*/libglib-*
+   ...
+   fun:call_init
+   fun:_dl_init
+}
+
+{
+   glib - hashtable new malloc 2
+   Memcheck:Leak
+   match-leak-kinds: reachable
+   fun:malloc
+   fun:g_malloc
+   ...
+   obj:*/libglib-*
+   ...
+   fun:call_init
+   fun:_dl_init
+}
+
+{
+   glib - hashtable new realloc
+   Memcheck:Leak
+   match-leak-kinds: reachable
+   fun:realloc
+   fun:g_realloc
+   ...
+   fun:g_hash_table_new_full
+   ...
+   obj:*/libglib-*
+   ...
+   fun:call_init
    fun:_dl_init
 }
diff --git a/daemons/attrd/attrd_cib.c b/daemons/attrd/attrd_cib.c
index e16e654cef..d62fe3d935 100644
--- a/daemons/attrd/attrd_cib.c
+++ b/daemons/attrd/attrd_cib.c
@@ -1,682 +1,683 @@
 /*
  * Copyright 2013-2024 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 <crm_internal.h>
 
 #include <errno.h>
 #include <inttypes.h>   // PRIu32
 #include <stdbool.h>
 #include <stdlib.h>
 #include <glib.h>
 
 #include <crm/cib/internal.h>       // cib__*
 #include <crm/common/logging.h>
 #include <crm/common/results.h>
 #include <crm/common/strings_internal.h>
 #include <crm/common/xml.h>
 #include <crm/cluster/internal.h>   // pcmk__get_node()
 
 #include "pacemaker-attrd.h"
 
 static int last_cib_op_done = 0;
 
 static void write_attribute(attribute_t *a, bool ignore_delay);
 
 static void
 attrd_cib_destroy_cb(gpointer user_data)
 {
     cib_t *cib = user_data;
 
     cib->cmds->signoff(cib);
 
     if (attrd_shutting_down(false)) {
         crm_info("Disconnected from the CIB manager");
 
     } else {
         // @TODO This should trigger a reconnect, not a shutdown
         crm_crit("Lost connection to the CIB manager, shutting down");
         attrd_exit_status = CRM_EX_DISCONNECT;
         attrd_shutdown(0);
     }
 }
 
 static void
 attrd_cib_updated_cb(const char *event, xmlNode *msg)
 {
     const xmlNode *patchset = NULL;
     const char *client_name = NULL;
     bool status_changed = false;
 
     if (attrd_shutting_down(true)) {
         crm_debug("Ignoring CIB change during shutdown");
         return;
     }
 
     if (cib__get_notify_patchset(msg, &patchset) != pcmk_rc_ok) {
         return;
     }
 
     if (cib__element_in_patchset(patchset, PCMK_XE_ALERTS)) {
         mainloop_set_trigger(attrd_config_read);
     }
 
     status_changed = cib__element_in_patchset(patchset, PCMK_XE_STATUS);
 
     client_name = crm_element_value(msg, PCMK__XA_CIB_CLIENTNAME);
     if (!cib__client_triggers_refresh(client_name)) {
         /* This change came from a source that ensured the CIB is consistent
          * with our attributes table, so we don't need to write anything out.
          */
         return;
     }
 
     if (!attrd_election_won()) {
         // Don't write attributes if we're not the writer
         return;
     }
 
     if (status_changed || cib__element_in_patchset(patchset, PCMK_XE_NODES)) {
         /* An unsafe client modified the PCMK_XE_NODES or PCMK_XE_STATUS
          * section. Write transient attributes to ensure they're up-to-date in
          * the CIB.
          */
         if (client_name == NULL) {
             client_name = crm_element_value(msg, PCMK__XA_CIB_CLIENTID);
         }
         crm_notice("Updating all attributes after %s event triggered by %s",
                    event, pcmk__s(client_name, "(unidentified client)"));
 
         attrd_write_attributes(attrd_write_all);
     }
 }
 
 int
 attrd_cib_connect(int max_retry)
 {
     static int attempts = 0;
 
     int rc = -ENOTCONN;
 
     the_cib = cib_new();
     if (the_cib == NULL) {
         return -ENOTCONN;
     }
 
     do {
         if (attempts > 0) {
             sleep(attempts);
         }
         attempts++;
         crm_debug("Connection attempt %d to the CIB manager", attempts);
         rc = the_cib->cmds->signon(the_cib, PCMK__VALUE_ATTRD, cib_command);
 
     } while ((rc != pcmk_ok) && (attempts < max_retry));
 
     if (rc != pcmk_ok) {
         crm_err("Connection to the CIB manager failed: %s " QB_XS " rc=%d",
                 pcmk_strerror(rc), rc);
         goto cleanup;
     }
 
     crm_debug("Connected to the CIB manager after %d attempts", attempts);
 
     rc = the_cib->cmds->set_connection_dnotify(the_cib, attrd_cib_destroy_cb);
     if (rc != pcmk_ok) {
         crm_err("Could not set disconnection callback");
         goto cleanup;
     }
 
     rc = the_cib->cmds->add_notify_callback(the_cib,
                                             PCMK__VALUE_CIB_DIFF_NOTIFY,
                                             attrd_cib_updated_cb);
     if (rc != pcmk_ok) {
         crm_err("Could not set CIB notification callback");
         goto cleanup;
     }
 
     return pcmk_ok;
 
 cleanup:
     cib__clean_up_connection(&the_cib);
     return -ENOTCONN;
 }
 
 void
 attrd_cib_disconnect(void)
 {
     CRM_CHECK(the_cib != NULL, return);
     the_cib->cmds->del_notify_callback(the_cib, PCMK__VALUE_CIB_DIFF_NOTIFY,
                                        attrd_cib_updated_cb);
     cib__clean_up_connection(&the_cib);
+    mainloop_destroy_trigger(attrd_config_read);
 }
 
 static void
 attrd_erase_cb(xmlNode *msg, int call_id, int rc, xmlNode *output,
                void *user_data)
 {
     const char *node = pcmk__s((const char *) user_data, "a node");
 
     if (rc == pcmk_ok) {
         crm_info("Cleared transient node attributes for %s from CIB", node);
     } else {
         crm_err("Unable to clear transient node attributes for %s from CIB: %s",
                 node, pcmk_strerror(rc));
     }
 }
 
 #define XPATH_TRANSIENT "//" PCMK__XE_NODE_STATE    \
                         "[@" PCMK_XA_UNAME "='%s']" \
                         "/" PCMK__XE_TRANSIENT_ATTRIBUTES
 
 /*!
  * \internal
  * \brief Wipe all transient node attributes for a node from the CIB
  *
  * \param[in] node  Node to clear attributes for
  */
 void
 attrd_cib_erase_transient_attrs(const char *node)
 {
     int call_id = 0;
     char *xpath = NULL;
 
     CRM_CHECK(node != NULL, return);
 
     xpath = crm_strdup_printf(XPATH_TRANSIENT, node);
 
     crm_debug("Clearing transient node attributes for %s from CIB using %s",
               node, xpath);
 
     call_id = the_cib->cmds->remove(the_cib, xpath, NULL, cib_xpath);
     free(xpath);
 
     the_cib->cmds->register_callback_full(the_cib, call_id, 120, FALSE,
                                           pcmk__str_copy(node),
                                           "attrd_erase_cb", attrd_erase_cb,
                                           free);
 }
 
 /*!
  * \internal
  * \brief Prepare the CIB after cluster is connected
  */
 void
 attrd_cib_init(void)
 {
     /* We have no attribute values in memory, so wipe the CIB to match. This is
      * normally done by the DC's controller when this node leaves the cluster, but
      * this handles the case where the node restarted so quickly that the
      * cluster layer didn't notice.
      *
      * \todo If pacemaker-attrd respawns after crashing (see PCMK_ENV_RESPAWNED),
      *       ideally we'd skip this and sync our attributes from the writer.
      *       However, currently we reject any values for us that the writer has, in
      *       attrd_peer_update().
      */
     attrd_cib_erase_transient_attrs(attrd_cluster->uname);
 
     // Set a trigger for reading the CIB (for the alerts section)
     attrd_config_read = mainloop_add_trigger(G_PRIORITY_HIGH, attrd_read_options, NULL);
 
     // Always read the CIB at start-up
     mainloop_set_trigger(attrd_config_read);
 }
 
 static gboolean
 attribute_timer_cb(gpointer data)
 {
     attribute_t *a = data;
     crm_trace("Dampen interval expired for %s", a->id);
     attrd_write_or_elect_attribute(a);
     return FALSE;
 }
 
 static void
 attrd_cib_callback(xmlNode *msg, int call_id, int rc, xmlNode *output, void *user_data)
 {
     int level = LOG_ERR;
     GHashTableIter iter;
     const char *peer = NULL;
     attribute_value_t *v = NULL;
 
     char *name = user_data;
     attribute_t *a = g_hash_table_lookup(attributes, name);
 
     if(a == NULL) {
         crm_info("Attribute %s no longer exists", name);
         return;
     }
 
     a->update = 0;
     if (rc == pcmk_ok && call_id < 0) {
         rc = call_id;
     }
 
     switch (rc) {
         case pcmk_ok:
             level = LOG_INFO;
             last_cib_op_done = call_id;
             if (a->timer && !a->timeout_ms) {
                 // Remove temporary dampening for failed writes
                 mainloop_timer_del(a->timer);
                 a->timer = NULL;
             }
             break;
 
         case -pcmk_err_diff_failed:    /* When an attr changes while the CIB is syncing */
         case -ETIME:           /* When an attr changes while there is a DC election */
         case -ENXIO:           /* When an attr changes while the CIB is syncing a
                                 *   newer config from a node that just came up
                                 */
             level = LOG_WARNING;
             break;
     }
 
     do_crm_log(level, "CIB update %d result for %s: %s " QB_XS " rc=%d",
                call_id, a->id, pcmk_strerror(rc), rc);
 
     g_hash_table_iter_init(&iter, a->values);
     while (g_hash_table_iter_next(&iter, (gpointer *) & peer, (gpointer *) & v)) {
         if (rc == pcmk_ok) {
             crm_info("* Wrote %s[%s]=%s",
                      a->id, peer, pcmk__s(v->requested, "(unset)"));
             pcmk__str_update(&(v->requested), NULL);
         } else {
             do_crm_log(level, "* Could not write %s[%s]=%s",
                        a->id, peer, pcmk__s(v->requested, "(unset)"));
             /* Reattempt write below if we are still the writer */
             attrd_set_attr_flags(a, attrd_attr_changed);
         }
     }
 
     if (pcmk_is_set(a->flags, attrd_attr_changed) && attrd_election_won()) {
         if (rc == pcmk_ok) {
             /* We deferred a write of a new update because this update was in
              * progress. Write out the new value without additional delay.
              */
             crm_debug("Pending update for %s can be written now", a->id);
             write_attribute(a, false);
 
         /* We're re-attempting a write because the original failed; delay
          * the next attempt so we don't potentially flood the CIB manager
          * and logs with a zillion attempts per second.
          *
          * @TODO We could elect a new writer instead. However, we'd have to
          * somehow downgrade our vote, and we'd still need something like this
          * if all peers similarly fail to write this attribute (which may
          * indicate a corrupted attribute entry rather than a CIB issue).
          */
         } else if (a->timer) {
             // Attribute has a dampening value, so use that as delay
             if (!mainloop_timer_running(a->timer)) {
                 crm_trace("Delayed re-attempted write for %s by %s",
                           name, pcmk__readable_interval(a->timeout_ms));
                 mainloop_timer_start(a->timer);
             }
         } else {
             /* Set a temporary dampening of 2 seconds (timer will continue
              * to exist until the attribute's dampening gets set or the
              * write succeeds).
              */
             a->timer = attrd_add_timer(a->id, 2000, a);
             mainloop_timer_start(a->timer);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Add a set-attribute update request to the current CIB transaction
  *
  * \param[in] attr     Attribute to update
  * \param[in] attr_id  ID of attribute to update
  * \param[in] node_id  ID of node for which to update attribute value
  * \param[in] set_id   ID of attribute set
  * \param[in] value    New value for attribute
  *
  * \return Standard Pacemaker return code
  */
 static int
 add_set_attr_update(const attribute_t *attr, const char *attr_id,
                     const char *node_id, const char *set_id, const char *value)
 {
     xmlNode *update = pcmk__xe_create(NULL, PCMK__XE_NODE_STATE);
     xmlNode *child = update;
     int rc = ENOMEM;
 
     crm_xml_add(child, PCMK_XA_ID, node_id);
 
     child = pcmk__xe_create(child, PCMK__XE_TRANSIENT_ATTRIBUTES);
     crm_xml_add(child, PCMK_XA_ID, node_id);
 
     child = pcmk__xe_create(child, attr->set_type);
     crm_xml_add(child, PCMK_XA_ID, set_id);
 
     child = pcmk__xe_create(child, PCMK_XE_NVPAIR);
     crm_xml_add(child, PCMK_XA_ID, attr_id);
     crm_xml_add(child, PCMK_XA_NAME, attr->id);
     crm_xml_add(child, PCMK_XA_VALUE, value);
 
     rc = the_cib->cmds->modify(the_cib, PCMK_XE_STATUS, update,
                                cib_can_create|cib_transaction);
     rc = pcmk_legacy2rc(rc);
 
     pcmk__xml_free(update);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Add an unset-attribute update request to the current CIB transaction
  *
  * \param[in] attr     Attribute to update
  * \param[in] attr_id  ID of attribute to update
  * \param[in] node_id  ID of node for which to update attribute value
  * \param[in] set_id   ID of attribute set
  *
  * \return Standard Pacemaker return code
  */
 static int
 add_unset_attr_update(const attribute_t *attr, const char *attr_id,
                       const char *node_id, const char *set_id)
 {
     char *xpath = crm_strdup_printf("/" PCMK_XE_CIB
                                     "/" PCMK_XE_STATUS
                                     "/" PCMK__XE_NODE_STATE
                                         "[@" PCMK_XA_ID "='%s']"
                                     "/" PCMK__XE_TRANSIENT_ATTRIBUTES
                                         "[@" PCMK_XA_ID "='%s']"
                                     "/%s[@" PCMK_XA_ID "='%s']"
                                     "/" PCMK_XE_NVPAIR
                                         "[@" PCMK_XA_ID "='%s' "
                                          "and @" PCMK_XA_NAME "='%s']",
                                     node_id, node_id, attr->set_type, set_id,
                                     attr_id, attr->id);
 
     int rc = the_cib->cmds->remove(the_cib, xpath, NULL,
                                    cib_xpath|cib_transaction);
 
     free(xpath);
     return pcmk_legacy2rc(rc);
 }
 
 /*!
  * \internal
  * \brief Add an attribute update request to the current CIB transaction
  *
  * \param[in] attr      Attribute to update
  * \param[in] value     New value for attribute
  * \param[in] node_id   ID of node for which to update attribute value
  *
  * \return Standard Pacemaker return code
  */
 static int
 add_attr_update(const attribute_t *attr, const char *value, const char *node_id)
 {
     char *set_id = attrd_set_id(attr, node_id);
     char *nvpair_id = attrd_nvpair_id(attr, node_id);
     int rc = pcmk_rc_ok;
 
     if (value == NULL) {
         rc = add_unset_attr_update(attr, nvpair_id, node_id, set_id);
     } else {
         rc = add_set_attr_update(attr, nvpair_id, node_id, set_id, value);
     }
     free(set_id);
     free(nvpair_id);
     return rc;
 }
 
 static void
 send_alert_attributes_value(attribute_t *a, GHashTable *t)
 {
     int rc = 0;
     attribute_value_t *at = NULL;
     GHashTableIter vIter;
 
     g_hash_table_iter_init(&vIter, t);
 
     while (g_hash_table_iter_next(&vIter, NULL, (gpointer *) & at)) {
         rc = attrd_send_attribute_alert(at->nodename, at->nodeid,
                                         a->id, at->current);
         crm_trace("Sent alerts for %s[%s]=%s: nodeid=%d rc=%d",
                   a->id, at->nodename, at->current, at->nodeid, rc);
     }
 }
 
 static void
 set_alert_attribute_value(GHashTable *t, attribute_value_t *v)
 {
     attribute_value_t *a_v = pcmk__assert_alloc(1, sizeof(attribute_value_t));
 
     a_v->nodeid = v->nodeid;
     a_v->nodename = pcmk__str_copy(v->nodename);
     a_v->current = pcmk__str_copy(v->current);
 
     g_hash_table_replace(t, a_v->nodename, a_v);
 }
 
 mainloop_timer_t *
 attrd_add_timer(const char *id, int timeout_ms, attribute_t *attr)
 {
    return mainloop_timer_add(id, timeout_ms, FALSE, attribute_timer_cb, attr);
 }
 
 /*!
  * \internal
  * \brief Write an attribute's values to the CIB if appropriate
  *
  * \param[in,out] a             Attribute to write
  * \param[in]     ignore_delay  If true, write attribute now regardless of any
  *                              configured delay
  */
 static void
 write_attribute(attribute_t *a, bool ignore_delay)
 {
     int private_updates = 0, cib_updates = 0;
     attribute_value_t *v = NULL;
     GHashTableIter iter;
     GHashTable *alert_attribute_value = NULL;
     int rc = pcmk_ok;
 
     if (a == NULL) {
         return;
     }
 
     /* If this attribute will be written to the CIB ... */
     if (!stand_alone && !pcmk_is_set(a->flags, attrd_attr_is_private)) {
         /* Defer the write if now's not a good time */
         if (a->update && (a->update < last_cib_op_done)) {
             crm_info("Write out of '%s' continuing: update %d considered lost",
                      a->id, a->update);
             a->update = 0; // Don't log this message again
 
         } else if (a->update) {
             crm_info("Write out of '%s' delayed: update %d in progress",
                      a->id, a->update);
             goto done;
 
         } else if (mainloop_timer_running(a->timer)) {
             if (ignore_delay) {
                 mainloop_timer_stop(a->timer);
                 crm_debug("Overriding '%s' write delay", a->id);
             } else {
                 crm_info("Delaying write of '%s'", a->id);
                 goto done;
             }
         }
 
         // Initiate a transaction for all the peer value updates
         CRM_CHECK(the_cib != NULL, goto done);
         the_cib->cmds->set_user(the_cib, a->user);
         rc = the_cib->cmds->init_transaction(the_cib);
         if (rc != pcmk_ok) {
             crm_err("Failed to write %s (set %s): Could not initiate "
                     "CIB transaction",
                     a->id, pcmk__s(a->set_id, "unspecified"));
             goto done;
         }
     }
 
     /* Attribute will be written shortly, so clear changed flag and force
      * write flag, and initialize UUID missing flag to false.
      */
     attrd_clear_attr_flags(a, attrd_attr_changed|attrd_attr_uuid_missing|attrd_attr_force_write);
 
     /* Make the table for the attribute trap */
     alert_attribute_value = pcmk__strikey_table(NULL,
                                                 attrd_free_attribute_value);
 
     /* Iterate over each peer value of this attribute */
     g_hash_table_iter_init(&iter, a->values);
     while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &v)) {
         const char *uuid = NULL;
 
         if (pcmk_is_set(v->flags, attrd_value_remote)) {
             /* If this is a Pacemaker Remote node, the node's UUID is the same
              * as its name, which we already have.
              */
             uuid = v->nodename;
 
         } else {
             // This will create a cluster node cache entry if none exists
             crm_node_t *peer = pcmk__get_node(v->nodeid, v->nodename, NULL,
                                               pcmk__node_search_any);
 
             uuid = peer->uuid;
 
             // Remember peer's node ID if we're just now learning it
             if ((peer->id != 0) && (v->nodeid == 0)) {
                 crm_trace("Learned ID %u for node %s", peer->id, v->nodename);
                 v->nodeid = peer->id;
             }
         }
 
         /* If this is a private attribute, no update needs to be sent */
         if (stand_alone || pcmk_is_set(a->flags, attrd_attr_is_private)) {
             private_updates++;
             continue;
         }
 
         // Defer write if this is a cluster node that's never been seen
         if (uuid == NULL) {
             attrd_set_attr_flags(a, attrd_attr_uuid_missing);
             crm_notice("Cannot update %s[%s]='%s' now because node's UUID is "
                        "unknown (will retry if learned)",
                        a->id, v->nodename, v->current);
             continue;
         }
 
         // Update this value as part of the CIB transaction we're building
         rc = add_attr_update(a, v->current, uuid);
         if (rc != pcmk_rc_ok) {
             crm_err("Failed to update %s[%s]='%s': %s "
                     QB_XS " node uuid=%s id=%" PRIu32,
                     a->id, v->nodename, v->current, pcmk_rc_str(rc),
                     uuid, v->nodeid);
             continue;
         }
 
         crm_debug("Writing %s[%s]=%s (node-state-id=%s node-id=%" PRIu32 ")",
                   a->id, v->nodename, pcmk__s(v->current, "(unset)"),
                   uuid, v->nodeid);
         cib_updates++;
 
         /* Preservation of the attribute to transmit alert */
         set_alert_attribute_value(alert_attribute_value, v);
 
         // Save this value so we can log it when write completes
         pcmk__str_update(&(v->requested), v->current);
     }
 
     if (private_updates) {
         crm_info("Processed %d private change%s for %s (set %s)",
                  private_updates, pcmk__plural_s(private_updates),
                  a->id, pcmk__s(a->set_id, "unspecified"));
     }
     if (cib_updates > 0) {
         char *id = pcmk__str_copy(a->id);
 
         // Commit transaction
         a->update = the_cib->cmds->end_transaction(the_cib, true, cib_none);
 
         crm_info("Sent CIB request %d with %d change%s for %s (set %s)",
                  a->update, cib_updates, pcmk__plural_s(cib_updates),
                  a->id, pcmk__s(a->set_id, "unspecified"));
 
         if (the_cib->cmds->register_callback_full(the_cib, a->update,
                                                   CIB_OP_TIMEOUT_S, FALSE, id,
                                                   "attrd_cib_callback",
                                                   attrd_cib_callback, free)) {
             // Transmit alert of the attribute
             send_alert_attributes_value(a, alert_attribute_value);
         }
     }
 
 done:
     // Discard transaction (if any)
     if (the_cib != NULL) {
         the_cib->cmds->end_transaction(the_cib, false, cib_none);
         the_cib->cmds->set_user(the_cib, NULL);
     }
 
     if (alert_attribute_value != NULL) {
         g_hash_table_destroy(alert_attribute_value);
     }
 }
 
 /*!
  * \internal
  * \brief Write out attributes
  *
  * \param[in] options  Group of enum attrd_write_options
  */
 void
 attrd_write_attributes(uint32_t options)
 {
     GHashTableIter iter;
     attribute_t *a = NULL;
 
     crm_debug("Writing out %s attributes",
               pcmk_is_set(options, attrd_write_all)? "all" : "changed");
     g_hash_table_iter_init(&iter, attributes);
     while (g_hash_table_iter_next(&iter, NULL, (gpointer *) & a)) {
         if (!pcmk_is_set(options, attrd_write_all) &&
             pcmk_is_set(a->flags, attrd_attr_uuid_missing)) {
             // Try writing this attribute again, in case peer ID was learned
             attrd_set_attr_flags(a, attrd_attr_changed);
         } else if (pcmk_is_set(a->flags, attrd_attr_force_write)) {
             /* If the force_write flag is set, write the attribute. */
             attrd_set_attr_flags(a, attrd_attr_changed);
         }
 
         if (pcmk_is_set(options, attrd_write_all) ||
             pcmk_is_set(a->flags, attrd_attr_changed)) {
             bool ignore_delay = pcmk_is_set(options, attrd_write_no_delay);
 
             if (pcmk_is_set(a->flags, attrd_attr_force_write)) {
                 // Always ignore delay when forced write flag is set
                 ignore_delay = true;
             }
             write_attribute(a, ignore_delay);
         } else {
             crm_trace("Skipping unchanged attribute %s", a->id);
         }
     }
 }
 
 void
 attrd_write_or_elect_attribute(attribute_t *a)
 {
     if (attrd_election_won()) {
         write_attribute(a, false);
     } else {
         attrd_start_election_if_needed();
     }
 }
diff --git a/daemons/attrd/attrd_ipc.c b/daemons/attrd/attrd_ipc.c
index 50831b9049..03f257aa3c 100644
--- a/daemons/attrd/attrd_ipc.c
+++ b/daemons/attrd/attrd_ipc.c
@@ -1,624 +1,627 @@
 /*
  * Copyright 2004-2024 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 <crm_internal.h>
 
 #include <errno.h>
 #include <stdint.h>
 #include <stdlib.h>
 #include <sys/types.h>
 
 #include <crm/cluster.h>
 #include <crm/cluster/internal.h>
 #include <crm/common/acl_internal.h>
 #include <crm/common/ipc_internal.h>
 #include <crm/common/logging.h>
 #include <crm/common/results.h>
 #include <crm/common/strings_internal.h>
 #include <crm/common/util.h>
 #include <crm/common/xml.h>
 
 #include "pacemaker-attrd.h"
 
 static qb_ipcs_service_t *ipcs = NULL;
 
 /*!
  * \internal
  * \brief Build the XML reply to a client query
  *
  * \param[in] attr Name of requested attribute
  * \param[in] host Name of requested host (or NULL for all hosts)
  *
  * \return New XML reply
  * \note Caller is responsible for freeing the resulting XML
  */
 static xmlNode *build_query_reply(const char *attr, const char *host)
 {
     xmlNode *reply = pcmk__xe_create(NULL, __func__);
     attribute_t *a;
 
     crm_xml_add(reply, PCMK__XA_T, PCMK__VALUE_ATTRD);
     crm_xml_add(reply, PCMK__XA_SUBT, PCMK__ATTRD_CMD_QUERY);
     crm_xml_add(reply, PCMK__XA_ATTR_VERSION, ATTRD_PROTOCOL_VERSION);
 
     /* If desired attribute exists, add its value(s) to the reply */
     a = g_hash_table_lookup(attributes, attr);
     if (a) {
         attribute_value_t *v;
         xmlNode *host_value;
 
         crm_xml_add(reply, PCMK__XA_ATTR_NAME, attr);
 
         /* Allow caller to use "localhost" to refer to local node */
         if (pcmk__str_eq(host, "localhost", pcmk__str_casei)) {
             host = attrd_cluster->uname;
             crm_trace("Mapped localhost to %s", host);
         }
 
         /* If a specific node was requested, add its value */
         if (host) {
             v = g_hash_table_lookup(a->values, host);
             host_value = pcmk__xe_create(reply, PCMK_XE_NODE);
             pcmk__xe_add_node(host_value, host, 0);
             crm_xml_add(host_value, PCMK__XA_ATTR_VALUE,
                         (v? v->current : NULL));
 
         /* Otherwise, add all nodes' values */
         } else {
             GHashTableIter iter;
 
             g_hash_table_iter_init(&iter, a->values);
             while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &v)) {
                 host_value = pcmk__xe_create(reply, PCMK_XE_NODE);
                 pcmk__xe_add_node(host_value, v->nodename, 0);
                 crm_xml_add(host_value, PCMK__XA_ATTR_VALUE, v->current);
             }
         }
     }
     return reply;
 }
 
 xmlNode *
 attrd_client_clear_failure(pcmk__request_t *request)
 {
     xmlNode *xml = request->xml;
     const char *rsc, *op, *interval_spec;
 
     if (minimum_protocol_version >= 2) {
         /* Propagate to all peers (including ourselves).
          * This ends up at attrd_peer_message().
          */
         attrd_send_message(NULL, xml, false);
         pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
         return NULL;
     }
 
     rsc = crm_element_value(xml, PCMK__XA_ATTR_RESOURCE);
     op = crm_element_value(xml, PCMK__XA_ATTR_CLEAR_OPERATION);
     interval_spec = crm_element_value(xml, PCMK__XA_ATTR_CLEAR_INTERVAL);
 
     /* Map this to an update */
     crm_xml_add(xml, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE);
 
     /* Add regular expression matching desired attributes */
 
     if (rsc) {
         char *pattern;
 
         if (op == NULL) {
             pattern = crm_strdup_printf(ATTRD_RE_CLEAR_ONE, rsc);
 
         } else {
             guint interval_ms = 0U;
 
             pcmk_parse_interval_spec(interval_spec, &interval_ms);
             pattern = crm_strdup_printf(ATTRD_RE_CLEAR_OP,
                                         rsc, op, interval_ms);
         }
 
         crm_xml_add(xml, PCMK__XA_ATTR_REGEX, pattern);
         free(pattern);
 
     } else {
         crm_xml_add(xml, PCMK__XA_ATTR_REGEX, ATTRD_RE_CLEAR_ALL);
     }
 
     /* Make sure attribute and value are not set, so we delete via regex */
     pcmk__xe_remove_attr(xml, PCMK__XA_ATTR_NAME);
     pcmk__xe_remove_attr(xml, PCMK__XA_ATTR_VALUE);
 
     return attrd_client_update(request);
 }
 
 xmlNode *
 attrd_client_peer_remove(pcmk__request_t *request)
 {
     xmlNode *xml = request->xml;
 
     // Host and ID are not used in combination, rather host has precedence
     const char *host = crm_element_value(xml, PCMK__XA_ATTR_HOST);
     char *host_alloc = NULL;
 
     attrd_send_ack(request->ipc_client, request->ipc_id, request->ipc_flags);
 
     if (host == NULL) {
         int nodeid = 0;
 
         crm_element_value_int(xml, PCMK__XA_ATTR_HOST_ID, &nodeid);
         if (nodeid > 0) {
             crm_node_t *node = NULL;
             char *host_alloc = NULL;
 
             node = pcmk__search_node_caches(nodeid, NULL,
                                             pcmk__node_search_cluster_member);
             if (node && node->uname) {
                 // Use cached name if available
                 host = node->uname;
             } else {
                 // Otherwise ask cluster layer
                 host_alloc = pcmk__cluster_node_name(nodeid);
                 host = host_alloc;
             }
             pcmk__xe_add_node(xml, host, 0);
         }
     }
 
     if (host) {
         crm_info("Client %s is requesting all values for %s be removed",
                  pcmk__client_name(request->ipc_client), host);
         attrd_send_message(NULL, xml, false); /* ends up at attrd_peer_message() */
         free(host_alloc);
     } else {
         crm_info("Ignoring request by client %s to remove all peer values without specifying peer",
                  pcmk__client_name(request->ipc_client));
     }
 
     pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
     return NULL;
 }
 
 xmlNode *
 attrd_client_query(pcmk__request_t *request)
 {
     xmlNode *query = request->xml;
     xmlNode *reply = NULL;
     const char *attr = NULL;
 
     crm_debug("Query arrived from %s", pcmk__client_name(request->ipc_client));
 
     /* Request must specify attribute name to query */
     attr = crm_element_value(query, PCMK__XA_ATTR_NAME);
     if (attr == NULL) {
         pcmk__format_result(&request->result, CRM_EX_ERROR, PCMK_EXEC_ERROR,
                             "Ignoring malformed query from %s (no attribute name given)",
                             pcmk__client_name(request->ipc_client));
         return NULL;
     }
 
     /* Build the XML reply */
     reply = build_query_reply(attr,
                               crm_element_value(query, PCMK__XA_ATTR_HOST));
     if (reply == NULL) {
         pcmk__format_result(&request->result, CRM_EX_ERROR, PCMK_EXEC_ERROR,
                             "Could not respond to query from %s: could not create XML reply",
                             pcmk__client_name(request->ipc_client));
         return NULL;
     } else {
         pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
     }
 
     request->ipc_client->request_id = 0;
     return reply;
 }
 
 xmlNode *
 attrd_client_refresh(pcmk__request_t *request)
 {
     crm_info("Updating all attributes");
 
     attrd_send_ack(request->ipc_client, request->ipc_id, request->ipc_flags);
     attrd_write_attributes(attrd_write_all|attrd_write_no_delay);
 
     pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
     return NULL;
 }
 
 static void
 handle_missing_host(xmlNode *xml)
 {
     const char *host = crm_element_value(xml, PCMK__XA_ATTR_HOST);
 
     if (host == NULL) {
         crm_trace("Inferring host");
         pcmk__xe_add_node(xml, attrd_cluster->uname, attrd_cluster->nodeid);
     }
 }
 
 /* Convert a single IPC message with a regex into one with multiple children, one
  * for each regex match.
  */
 static int
 expand_regexes(xmlNode *xml, const char *attr, const char *value, const char *regex)
 {
     if (attr == NULL && regex) {
         bool matched = false;
         GHashTableIter aIter;
         regex_t r_patt;
 
         crm_debug("Setting %s to %s", regex, value);
         if (regcomp(&r_patt, regex, REG_EXTENDED|REG_NOSUB)) {
             return EINVAL;
         }
 
         g_hash_table_iter_init(&aIter, attributes);
         while (g_hash_table_iter_next(&aIter, (gpointer *) & attr, NULL)) {
             int status = regexec(&r_patt, attr, 0, NULL, 0);
 
             if (status == 0) {
                 xmlNode *child = pcmk__xe_create(xml, PCMK_XE_OP);
 
                 crm_trace("Matched %s with %s", attr, regex);
                 matched = true;
 
                 /* Copy all the non-conflicting attributes from the parent over,
                  * but remove the regex and replace it with the name.
                  */
                 pcmk__xe_copy_attrs(child, xml, pcmk__xaf_no_overwrite);
                 pcmk__xe_remove_attr(child, PCMK__XA_ATTR_REGEX);
                 crm_xml_add(child, PCMK__XA_ATTR_NAME, attr);
             }
         }
 
         regfree(&r_patt);
 
         /* Return a code if we never matched anything.  This should not be treated
          * as an error.  It indicates there was a regex, and it was a valid regex,
          * but simply did not match anything and the caller should not continue
          * doing any regex-related processing.
          */
         if (!matched) {
             return pcmk_rc_op_unsatisfied;
         }
 
     } else if (attr == NULL) {
         return pcmk_rc_bad_nvpair;
     }
 
     return pcmk_rc_ok;
 }
 
 static int
 handle_regexes(pcmk__request_t *request)
 {
     xmlNode *xml = request->xml;
     int rc = pcmk_rc_ok;
 
     const char *attr = crm_element_value(xml, PCMK__XA_ATTR_NAME);
     const char *value = crm_element_value(xml, PCMK__XA_ATTR_VALUE);
     const char *regex = crm_element_value(xml, PCMK__XA_ATTR_REGEX);
 
     rc = expand_regexes(xml, attr, value, regex);
 
     if (rc == EINVAL) {
         pcmk__format_result(&request->result, CRM_EX_ERROR, PCMK_EXEC_ERROR,
                             "Bad regex '%s' for update from client %s", regex,
                             pcmk__client_name(request->ipc_client));
 
     } else if (rc == pcmk_rc_bad_nvpair) {
         crm_err("Update request did not specify attribute or regular expression");
         pcmk__format_result(&request->result, CRM_EX_ERROR, PCMK_EXEC_ERROR,
                             "Client %s update request did not specify attribute or regular expression",
                             pcmk__client_name(request->ipc_client));
     }
 
     return rc;
 }
 
 static int
 handle_value_expansion(const char **value, xmlNode *xml, const char *op,
                        const char *attr)
 {
     attribute_t *a = g_hash_table_lookup(attributes, attr);
 
     if (a == NULL && pcmk__str_eq(op, PCMK__ATTRD_CMD_UPDATE_DELAY, pcmk__str_none)) {
         return EINVAL;
     }
 
     if (*value && attrd_value_needs_expansion(*value)) {
         int int_value;
         attribute_value_t *v = NULL;
 
         if (a) {
             const char *host = crm_element_value(xml, PCMK__XA_ATTR_HOST);
             v = g_hash_table_lookup(a->values, host);
         }
 
         int_value = attrd_expand_value(*value, (v? v->current : NULL));
 
         crm_info("Expanded %s=%s to %d", attr, *value, int_value);
         crm_xml_add_int(xml, PCMK__XA_ATTR_VALUE, int_value);
 
         /* Replacing the value frees the previous memory, so re-query it */
         *value = crm_element_value(xml, PCMK__XA_ATTR_VALUE);
     }
 
     return pcmk_rc_ok;
 }
 
 static void
 send_update_msg_to_cluster(pcmk__request_t *request, xmlNode *xml)
 {
     if (pcmk__str_eq(attrd_request_sync_point(xml), PCMK__VALUE_CLUSTER, pcmk__str_none)) {
         /* The client is waiting on the cluster-wide sync point.  In this case,
          * the response ACK is not sent until this attrd broadcasts the update
          * and receives its own confirmation back from all peers.
          */
         attrd_expect_confirmations(request, attrd_cluster_sync_point_update);
         attrd_send_message(NULL, xml, true); /* ends up at attrd_peer_message() */
 
     } else {
         /* The client is either waiting on the local sync point or was not
          * waiting on any sync point at all.  For the local sync point, the
          * response ACK is sent in attrd_peer_update.  For clients not
          * waiting on any sync point, the response ACK is sent in
          * handle_update_request immediately before this function was called.
          */
         attrd_send_message(NULL, xml, false); /* ends up at attrd_peer_message() */
     }
 }
 
 static int
 send_child_update(xmlNode *child, void *data)
 {
     pcmk__request_t *request = (pcmk__request_t *) data;
 
     /* Calling pcmk__set_result is handled by one of these calls to
      * attrd_client_update, so no need to do it again here.
      */
     request->xml = child;
     attrd_client_update(request);
     return pcmk_rc_ok;
 }
 
 xmlNode *
 attrd_client_update(pcmk__request_t *request)
 {
     xmlNode *xml = NULL;
     const char *attr, *value, *regex;
 
     CRM_CHECK((request != NULL) && (request->xml != NULL), return NULL);
 
     xml = request->xml;
 
     /* If the message has children, that means it is a message from a newer
      * client that supports sending multiple operations at a time.  There are
      * two ways we can handle that.
      */
     if (xml->children != NULL) {
         if (ATTRD_SUPPORTS_MULTI_MESSAGE(minimum_protocol_version)) {
             /* First, if all peers support a certain protocol version, we can
              * just broadcast the big message and they'll handle it.  However,
              * we also need to apply all the transformations in this function
              * to the children since they don't happen anywhere else.
              */
             for (xmlNode *child = pcmk__xe_first_child(xml, PCMK_XE_OP, NULL,
                                                        NULL);
                  child != NULL; child = pcmk__xe_next_same(child)) {
 
                 attr = crm_element_value(child, PCMK__XA_ATTR_NAME);
                 value = crm_element_value(child, PCMK__XA_ATTR_VALUE);
 
                 handle_missing_host(child);
 
                 if (handle_value_expansion(&value, child, request->op, attr) == EINVAL) {
                     pcmk__format_result(&request->result, CRM_EX_NOSUCH, PCMK_EXEC_ERROR,
                                         "Attribute %s does not exist", attr);
                     return NULL;
                 }
             }
 
             send_update_msg_to_cluster(request, xml);
             pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
 
         } else {
             /* Save the original xml node pointer so it can be restored after iterating
              * over all the children.
              */
             xmlNode *orig_xml = request->xml;
 
             /* Second, if they do not support that protocol version, split it
              * up into individual messages and call attrd_client_update on
              * each one.
              */
             pcmk__xe_foreach_child(xml, PCMK_XE_OP, send_child_update, request);
             request->xml = orig_xml;
         }
 
         return NULL;
     }
 
     attr = crm_element_value(xml, PCMK__XA_ATTR_NAME);
     value = crm_element_value(xml, PCMK__XA_ATTR_VALUE);
     regex = crm_element_value(xml, PCMK__XA_ATTR_REGEX);
 
     if (handle_regexes(request) != pcmk_rc_ok) {
         /* Error handling was already dealt with in handle_regexes, so just return. */
         return NULL;
     } else if (regex) {
         /* Recursively call attrd_client_update on the new message with regexes
          * expanded.  If supported by the attribute daemon, this means that all
          * matches can also be handled atomically.
          */
         return attrd_client_update(request);
     }
 
     handle_missing_host(xml);
 
     if (handle_value_expansion(&value, xml, request->op, attr) == EINVAL) {
         pcmk__format_result(&request->result, CRM_EX_NOSUCH, PCMK_EXEC_ERROR,
                             "Attribute %s does not exist", attr);
         return NULL;
     }
 
     crm_debug("Broadcasting %s[%s]=%s%s",
               attr, crm_element_value(xml, PCMK__XA_ATTR_HOST),
               value, (attrd_election_won()? " (writer)" : ""));
 
     send_update_msg_to_cluster(request, xml);
     pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL);
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Accept a new client IPC connection
  *
  * \param[in,out] c    New connection
  * \param[in]     uid  Client user id
  * \param[in]     gid  Client group id
  *
  * \return pcmk_ok on success, -errno otherwise
  */
 static int32_t
 attrd_ipc_accept(qb_ipcs_connection_t *c, uid_t uid, gid_t gid)
 {
     crm_trace("New client connection %p", c);
     if (attrd_shutting_down(false)) {
         crm_info("Ignoring new connection from pid %d during shutdown",
                  pcmk__client_pid(c));
         return -ECONNREFUSED;
     }
 
     if (pcmk__new_client(c, uid, gid) == NULL) {
         return -ENOMEM;
     }
     return pcmk_ok;
 }
 
 /*!
  * \internal
  * \brief Destroy a client IPC connection
  *
  * \param[in] c  Connection to destroy
  *
  * \return FALSE (i.e. do not re-run this callback)
  */
 static int32_t
 attrd_ipc_closed(qb_ipcs_connection_t *c)
 {
     pcmk__client_t *client = pcmk__find_client(c);
 
     if (client == NULL) {
         crm_trace("Ignoring request to clean up unknown connection %p", c);
     } else {
         crm_trace("Cleaning up closed client connection %p", c);
 
         /* Remove the client from the sync point waitlist if it's present. */
         attrd_remove_client_from_waitlist(client);
 
         /* And no longer wait for confirmations from any peers. */
         attrd_do_not_wait_for_client(client);
 
         pcmk__free_client(client);
     }
 
     return FALSE;
 }
 
 /*!
  * \internal
  * \brief Destroy a client IPC connection
  *
  * \param[in,out] c  Connection to destroy
  *
  * \note We handle a destroyed connection the same as a closed one,
  *       but we need a separate handler because the return type is different.
  */
 static void
 attrd_ipc_destroy(qb_ipcs_connection_t *c)
 {
     crm_trace("Destroying client connection %p", c);
     attrd_ipc_closed(c);
 }
 
 static int32_t
 attrd_ipc_dispatch(qb_ipcs_connection_t * c, void *data, size_t size)
 {
     uint32_t id = 0;
     uint32_t flags = 0;
     pcmk__client_t *client = pcmk__find_client(c);
     xmlNode *xml = NULL;
 
     // Sanity-check, and parse XML from IPC data
     CRM_CHECK((c != NULL) && (client != NULL), return 0);
     if (data == NULL) {
         crm_debug("No IPC data from PID %d", pcmk__client_pid(c));
         return 0;
     }
 
     xml = pcmk__client_data2xml(client, data, &id, &flags);
 
     if (xml == NULL) {
         crm_debug("Unrecognizable IPC data from PID %d", pcmk__client_pid(c));
         pcmk__ipc_send_ack(client, id, flags, PCMK__XE_ACK, NULL,
                            CRM_EX_PROTOCOL);
         return 0;
 
     } else {
         pcmk__request_t request = {
             .ipc_client     = client,
             .ipc_id         = id,
             .ipc_flags      = flags,
             .peer           = NULL,
             .xml            = xml,
             .call_options   = 0,
             .result         = PCMK__UNKNOWN_RESULT,
         };
 
         CRM_ASSERT(client->user != NULL);
         pcmk__update_acl_user(xml, PCMK__XA_ATTR_USER, client->user);
 
         request.op = crm_element_value_copy(request.xml, PCMK_XA_TASK);
         CRM_CHECK(request.op != NULL, return 0);
 
         attrd_handle_request(&request);
         pcmk__reset_request(&request);
     }
 
     pcmk__xml_free(xml);
     return 0;
 }
 
 static struct qb_ipcs_service_handlers ipc_callbacks = {
     .connection_accept = attrd_ipc_accept,
     .connection_created = NULL,
     .msg_process = attrd_ipc_dispatch,
     .connection_closed = attrd_ipc_closed,
     .connection_destroyed = attrd_ipc_destroy
 };
 
 void
 attrd_ipc_fini(void)
 {
     if (ipcs != NULL) {
         pcmk__drop_all_clients(ipcs);
         qb_ipcs_destroy(ipcs);
         ipcs = NULL;
     }
+
+    attrd_unregister_handlers();
+    pcmk__client_cleanup();
 }
 
 /*!
  * \internal
  * \brief Set up attrd IPC communication
  */
 void
 attrd_init_ipc(void)
 {
     pcmk__serve_attrd_ipc(&ipcs, &ipc_callbacks);
 }
diff --git a/daemons/attrd/pacemaker-attrd.c b/daemons/attrd/pacemaker-attrd.c
index 4ae5c8a555..829e4fe370 100644
--- a/daemons/attrd/pacemaker-attrd.c
+++ b/daemons/attrd/pacemaker-attrd.c
@@ -1,224 +1,225 @@
 /*
  * Copyright 2013-2024 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 <crm_internal.h>
 
 #include <sys/param.h>
 #include <stdio.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
 
 #include <stdlib.h>
 #include <errno.h>
 #include <fcntl.h>
 
 #include <crm/crm.h>
 #include <crm/pengine/rules.h>
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/iso8601.h>
 #include <crm/common/ipc.h>
 #include <crm/common/ipc_internal.h>
 #include <crm/common/output_internal.h>
 #include <crm/common/xml.h>
 #include <crm/cluster/internal.h>
 
 #include <crm/common/attrs_internal.h>
 #include "pacemaker-attrd.h"
 
 #define SUMMARY "daemon for managing Pacemaker node attributes"
 
 gboolean stand_alone = FALSE;
 gchar **log_files = NULL;
 
 static GOptionEntry entries[] = {
     { "stand-alone", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &stand_alone,
       "(Advanced use only) Run in stand-alone mode", NULL },
 
     { "logfile", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME_ARRAY,
       &log_files, "Send logs to the additional named logfile", NULL },
 
     { NULL }
 };
 
 static pcmk__output_t *out = NULL;
 
 static pcmk__supported_format_t formats[] = {
     PCMK__SUPPORTED_FORMAT_NONE,
     PCMK__SUPPORTED_FORMAT_TEXT,
     PCMK__SUPPORTED_FORMAT_XML,
     { NULL, NULL, NULL }
 };
 
 lrmd_t *the_lrmd = NULL;
 pcmk_cluster_t *attrd_cluster = NULL;
 crm_trigger_t *attrd_config_read = NULL;
 crm_exit_t attrd_exit_status = CRM_EX_OK;
 
 static bool
 ipc_already_running(void)
 {
     pcmk_ipc_api_t *old_instance = NULL;
     int rc = pcmk_rc_ok;
 
     rc = pcmk_new_ipc_api(&old_instance, pcmk_ipc_attrd);
     if (rc != pcmk_rc_ok) {
         return false;
     }
 
     rc = pcmk__connect_ipc(old_instance, pcmk_ipc_dispatch_sync, 2);
     if (rc != pcmk_rc_ok) {
         crm_debug("No existing %s manager instance found: %s",
                   pcmk_ipc_name(old_instance, true), pcmk_rc_str(rc));
         pcmk_free_ipc_api(old_instance);
         return false;
     }
 
     pcmk_disconnect_ipc(old_instance);
     pcmk_free_ipc_api(old_instance);
     return true;
 }
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
     GOptionContext *context = NULL;
 
     context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
     pcmk__add_main_args(context, entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     int rc = pcmk_rc_ok;
 
     GError *error = NULL;
     bool initialized = false;
 
     GOptionGroup *output_group = NULL;
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, NULL);
     GOptionContext *context = build_arg_context(args, &output_group);
 
     attrd_init_mainloop();
     crm_log_preinit(NULL, argc, argv);
     mainloop_add_signal(SIGTERM, attrd_shutdown);
 
     pcmk__register_formats(output_group, formats);
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         attrd_exit_status = CRM_EX_USAGE;
         goto done;
     }
 
     rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
     if ((rc != pcmk_rc_ok) || (out == NULL)) {
         attrd_exit_status = CRM_EX_ERROR;
         g_set_error(&error, PCMK__EXITC_ERROR, attrd_exit_status,
                     "Error creating output format %s: %s",
                     args->output_ty, pcmk_rc_str(rc));
         goto done;
     }
 
     if (args->version) {
         out->version(out, false);
         goto done;
     }
 
     // Open additional log files
     pcmk__add_logfiles(log_files, out);
 
     crm_log_init(PCMK__VALUE_ATTRD, LOG_INFO, TRUE, FALSE, argc, argv, FALSE);
     crm_notice("Starting Pacemaker node attribute manager%s",
                stand_alone ? " in standalone mode" : "");
 
     if (ipc_already_running()) {
         const char *msg = "pacemaker-attrd is already active, aborting startup";
 
         attrd_exit_status = CRM_EX_OK;
         g_set_error(&error, PCMK__EXITC_ERROR, attrd_exit_status, "%s", msg);
         crm_err("%s", msg);
         goto done;
     }
 
     initialized = true;
 
     attributes = pcmk__strkey_table(NULL, attrd_free_attribute);
 
     /* Connect to the CIB before connecting to the cluster or listening for IPC.
      * This allows us to assume the CIB is connected whenever we process a
      * cluster or IPC message (which also avoids start-up race conditions).
      */
     if (!stand_alone) {
         if (attrd_cib_connect(30) != pcmk_ok) {
             attrd_exit_status = CRM_EX_FATAL;
             g_set_error(&error, PCMK__EXITC_ERROR, attrd_exit_status,
                         "Could not connect to the CIB");
             goto done;
         }
         crm_info("CIB connection active");
     }
 
     if (attrd_cluster_connect() != pcmk_ok) {
         attrd_exit_status = CRM_EX_FATAL;
         g_set_error(&error, PCMK__EXITC_ERROR, attrd_exit_status,
                     "Could not connect to the cluster");
         goto done;
     }
     crm_info("Cluster connection active");
 
     // Initialization that requires the cluster to be connected
     attrd_election_init();
 
     if (!stand_alone) {
         attrd_cib_init();
     }
 
     /* Set a private attribute for ourselves with the protocol version we
      * support. This lets all nodes determine the minimum supported version
      * across all nodes. It also ensures that the writer learns our node name,
      * so it can send our attributes to the CIB.
      */
     attrd_broadcast_protocol();
 
     attrd_init_ipc();
     crm_notice("Pacemaker node attribute manager successfully started and accepting connections");
     attrd_run_mainloop();
 
   done:
     if (initialized) {
         crm_info("Shutting down attribute manager");
 
         attrd_election_fini();
         attrd_ipc_fini();
         attrd_lrmd_disconnect();
 
         if (!stand_alone) {
             attrd_cib_disconnect();
         }
 
         attrd_free_waitlist();
+        pcmk_cluster_disconnect(attrd_cluster);
         pcmk_cluster_free(attrd_cluster);
         g_hash_table_destroy(attributes);
     }
 
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
 
     g_strfreev(log_files);
 
     pcmk__output_and_clear_error(&error, out);
 
     if (out != NULL) {
         out->finish(out, attrd_exit_status, true, NULL);
         pcmk__output_free(out);
     }
     pcmk__unregister_formats();
     crm_exit(attrd_exit_status);
 }