diff --git a/lib/common/output.c b/lib/common/output.c
index 92fbfdac41..a58ff58533 100644
--- a/lib/common/output.c
+++ b/lib/common/output.c
@@ -1,337 +1,337 @@
 /*
  * Copyright 2019-2024 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 <crm_internal.h>
 
 #include <crm/common/util.h>
 #include <crm/common/xml.h>
 #include <libxml/tree.h>
 
 #include "crmcommon_private.h"
 
 static GHashTable *formatters = NULL;
 
 #if defined(PCMK__UNIT_TESTING)
 // LCOV_EXCL_START
 GHashTable *
 pcmk__output_formatters(void) {
     return formatters;
 }
 // LCOV_EXCL_STOP
 #endif
 
 void
 pcmk__output_free(pcmk__output_t *out) {
     if (out == NULL) {
         return;
     }
 
     out->free_priv(out);
 
     if (out->messages != NULL) {
         g_hash_table_destroy(out->messages);
     }
 
     g_free(out->request);
     free(out);
 }
 
 /*!
  * \internal
  * \brief Create a new \p pcmk__output_t structure
  *
  * This function does not register any message functions with the newly created
  * object.
  *
  * \param[in,out] out       Where to store the new output object
  * \param[in]     fmt_name  How to format output
  * \param[in]     filename  Where to write formatted output. This can be a
  *                          filename (the file will be overwritten if it already
  *                          exists), or \p NULL or \p "-" for stdout. For no
  *                          output, pass a filename of \p "/dev/null".
  * \param[in]     argv      List of command line arguments
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name,
                       const char *filename, char **argv)
 {
     pcmk__output_factory_t create = NULL;
 
     CRM_ASSERT(formatters != NULL && out != NULL);
 
     /* If no name was given, just try "text".  It's up to each tool to register
      * what it supports so this also may not be valid.
      */
     if (fmt_name == NULL) {
         create = g_hash_table_lookup(formatters, "text");
     } else {
         create = g_hash_table_lookup(formatters, fmt_name);
     }
 
     if (create == NULL) {
         return pcmk_rc_unknown_format;
     }
 
     *out = create(argv);
     if (*out == NULL) {
         return ENOMEM;
     }
 
     if (pcmk__str_eq(filename, "-", pcmk__str_null_matches)) {
         (*out)->dest = stdout;
     } else {
         (*out)->dest = fopen(filename, "w");
         if ((*out)->dest == NULL) {
             pcmk__output_free(*out);
             *out = NULL;
             return errno;
         }
     }
 
     (*out)->quiet = false;
     (*out)->messages = pcmk__strkey_table(free, NULL);
 
     if ((*out)->init(*out) == false) {
         pcmk__output_free(*out);
         return ENOMEM;
     }
 
     setenv("OCF_OUTPUT_FORMAT", (*out)->fmt_name, 1);
 
     return pcmk_rc_ok;
 }
 
 int
 pcmk__output_new(pcmk__output_t **out, const char *fmt_name,
                  const char *filename, char **argv)
 {
     int rc = pcmk__bare_output_new(out, fmt_name, filename, argv);
 
     if (rc == pcmk_rc_ok) {
         // Register libcrmcommon messages
         pcmk__register_option_messages(*out);
         pcmk__register_patchset_messages(*out);
     }
     return rc;
 }
 
 int
 pcmk__register_format(GOptionGroup *group, const char *name,
                       pcmk__output_factory_t create,
                       const GOptionEntry *options)
 {
     char *name_copy = NULL;
 
     CRM_ASSERT(create != NULL && !pcmk__str_empty(name));
 
     name_copy = strdup(name);
     if (name_copy == NULL) {
         return ENOMEM;
     }
 
     if (formatters == NULL) {
         formatters = pcmk__strkey_table(free, NULL);
     }
 
     if (options != NULL && group != NULL) {
         g_option_group_add_entries(group, options);
     }
 
     g_hash_table_insert(formatters, name_copy, create);
     return pcmk_rc_ok;
 }
 
 void
 pcmk__register_formats(GOptionGroup *group,
                        const pcmk__supported_format_t *formats)
 {
     if (formats == NULL) {
         return;
     }
     for (const pcmk__supported_format_t *entry = formats; entry->name != NULL;
          entry++) {
         pcmk__register_format(group, entry->name, entry->create, entry->options);
     }
 }
 
 void
 pcmk__unregister_formats(void) {
     if (formatters != NULL) {
         g_hash_table_destroy(formatters);
         formatters = NULL;
     }
 }
 
 int
 pcmk__call_message(pcmk__output_t *out, const char *message_id, ...) {
     va_list args;
     int rc = pcmk_rc_ok;
     pcmk__message_fn_t fn;
 
     CRM_ASSERT(out != NULL && !pcmk__str_empty(message_id));
 
     fn = g_hash_table_lookup(out->messages, message_id);
     if (fn == NULL) {
         crm_debug("Called unknown output message '%s' for format '%s'",
                   message_id, out->fmt_name);
         return EINVAL;
     }
 
     va_start(args, message_id);
     rc = fn(out, args);
     va_end(args);
 
     return rc;
 }
 
 void
 pcmk__register_message(pcmk__output_t *out, const char *message_id,
                        pcmk__message_fn_t fn) {
     CRM_ASSERT(out != NULL && !pcmk__str_empty(message_id) && fn != NULL);
 
     g_hash_table_replace(out->messages, pcmk__str_copy(message_id), fn);
 }
 
 void
 pcmk__register_messages(pcmk__output_t *out, const pcmk__message_entry_t *table)
 {
     for (const pcmk__message_entry_t *entry = table; entry->message_id != NULL;
          entry++) {
         if (pcmk__strcase_any_of(entry->fmt_name, "default", out->fmt_name, NULL)) {
             pcmk__register_message(out, entry->message_id, entry->fn);
         }
     }
 }
 
 void
 pcmk__output_and_clear_error(GError **error, pcmk__output_t *out)
 {
     if (error == NULL || *error == NULL) {
         return;
     }
 
     if (out != NULL) {
         out->err(out, "%s: %s", g_get_prgname(), (*error)->message);
     } else {
         fprintf(stderr, "%s: %s\n", g_get_prgname(), (*error)->message);
     }
 
     g_clear_error(error);
 }
 
 /*!
  * \internal
  * \brief Create an XML-only output object
  *
  * Create an output object that supports only the XML format, and free
  * existing XML if supplied (particularly useful for libpacemaker public API
  * functions that want to free any previous result supplied by the caller).
  *
  * \param[out]     out  Where to put newly created output object
  * \param[in,out]  xml  If \c *xml is non-NULL, this will be freed
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__xml_output_new(pcmk__output_t **out, xmlNodePtr *xml) {
     pcmk__supported_format_t xml_format[] = {
         PCMK__SUPPORTED_FORMAT_XML,
         { NULL, NULL, NULL }
     };
 
     if (xml == NULL) {
         return EINVAL;
     }
 
     if (*xml != NULL) {
-        xmlFreeNode(*xml);
+        free_xml(*xml);
         *xml = NULL;
     }
     pcmk__register_formats(NULL, xml_format);
     return pcmk__output_new(out, "xml", NULL, NULL);
 }
 
 /*!
  * \internal
  * \brief  Finish and free an XML-only output object
  *
  * \param[in,out] out         Output object to free
  * \param[in]     exit_status The exit value of the whole program
  * \param[out]    xml         If not NULL, where to store XML output
  */
 void
 pcmk__xml_output_finish(pcmk__output_t *out, crm_exit_t exit_status,
                         xmlNodePtr *xml)
 {
     if (out == NULL) {
         return;
     }
 
     out->finish(out, exit_status, FALSE, (void **) xml);
     pcmk__output_free(out);
 }
 
 /*!
  * \internal
  * \brief Create a new output object using the "log" format
  *
  * \param[out] out  Where to store newly allocated output object
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__log_output_new(pcmk__output_t **out)
 {
     int rc = pcmk_rc_ok;
     const char* argv[] = { "", NULL };
     pcmk__supported_format_t formats[] = {
         PCMK__SUPPORTED_FORMAT_LOG,
         { NULL, NULL, NULL }
     };
 
     pcmk__register_formats(NULL, formats);
     rc = pcmk__output_new(out, "log", NULL, (char **) argv);
     if ((rc != pcmk_rc_ok) || (*out == NULL)) {
         crm_err("Can't log certain messages due to internal error: %s",
                 pcmk_rc_str(rc));
         return rc;
     }
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Create a new output object using the "text" format
  *
  * \param[out] out       Where to store newly allocated output object
  * \param[in]  filename  Name of output destination file
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__text_output_new(pcmk__output_t **out, const char *filename)
 {
     int rc = pcmk_rc_ok;
     const char* argv[] = { "", NULL };
     pcmk__supported_format_t formats[] = {
         PCMK__SUPPORTED_FORMAT_TEXT,
         { NULL, NULL, NULL }
     };
 
     pcmk__register_formats(NULL, formats);
     rc = pcmk__output_new(out, "text", filename, (char **) argv);
     if ((rc != pcmk_rc_ok) || (*out == NULL)) {
         crm_err("Can't create text output object to internal error: %s",
                 pcmk_rc_str(rc));
         return rc;
     }
     return pcmk_rc_ok;
 }
diff --git a/lib/common/output_html.c b/lib/common/output_html.c
index afb2609df3..dc254a5b5f 100644
--- a/lib/common/output_html.c
+++ b/lib/common/output_html.c
@@ -1,519 +1,519 @@
 /*
  * Copyright 2019-2024 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 <crm_internal.h>
 
 #include <ctype.h>
 #include <libxml/HTMLtree.h>
 #include <stdarg.h>
 #include <stdlib.h>
 #include <stdio.h>
 
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/xml.h>
 
 static const char *stylesheet_default =
     "." PCMK__VALUE_BOLD " { font-weight: bold }\n"
 
     "." PCMK_VALUE_ONLINE " { color: green }\n"
     "." PCMK_VALUE_OFFLINE " { color: red }\n"
     "." PCMK__VALUE_MAINT " { color: blue }\n"
     "." PCMK_VALUE_STANDBY " { color: blue }\n"
     "." PCMK__VALUE_HEALTH_RED " { color: red }\n"
     "." PCMK__VALUE_HEALTH_YELLOW " { color: GoldenRod }\n"
 
     "." PCMK__VALUE_RSC_FAILED " { color: red }\n"
     "." PCMK__VALUE_RSC_FAILURE_IGNORED " { color: DarkGreen }\n"
     "." PCMK__VALUE_RSC_MANAGED " { color: blue }\n"
     "." PCMK__VALUE_RSC_MULTIPLE " { color: orange }\n"
     "." PCMK__VALUE_RSC_OK " { color: green }\n"
 
     "." PCMK__VALUE_WARNING " { color: red; font-weight: bold }";
 
 static gboolean cgi_output = FALSE;
 static char *stylesheet_link = NULL;
 static char *title = NULL;
 static GSList *extra_headers = NULL;
 
 GOptionEntry pcmk__html_output_entries[] = {
     { "html-cgi", 0, 0, G_OPTION_ARG_NONE, &cgi_output,
       "Add CGI headers (requires --output-as=html)",
       NULL },
 
     { "html-stylesheet", 0, 0, G_OPTION_ARG_STRING, &stylesheet_link,
       "Link to an external stylesheet (requires --output-as=html)",
       "URI" },
 
     { "html-title", 0, 0, G_OPTION_ARG_STRING, &title,
       "Specify a page title (requires --output-as=html)",
       "TITLE" },
 
     { NULL }
 };
 
 /* The first several elements of this struct must be the same as the first
  * several elements of private_data_s in lib/common/output_xml.c.  This
  * struct gets passed to a bunch of the pcmk__output_xml_* functions which
  * assume an XML private_data_s.  Keeping them laid out the same means this
  * still works.
  */
 typedef struct private_data_s {
     /* Begin members that must match the XML version */
     xmlNode *root;
     GQueue *parent_q;
     GSList *errors;
     /* End members that must match the XML version */
 } private_data_t;
 
 static void
 html_free_priv(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     if (out == NULL || out->priv == NULL) {
         return;
     }
 
     priv = out->priv;
 
     free_xml(priv->root);
     /* The elements of parent_q are xmlNodes that are a part of the
      * priv->root document, so the above line already frees them.  Don't
      * call g_queue_free_full here.
      */
     g_queue_free(priv->parent_q);
     g_slist_free_full(priv->errors, free);
     free(priv);
     out->priv = NULL;
 }
 
 static bool
 html_init(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL);
 
     /* If html_init was previously called on this output struct, just return. */
     if (out->priv != NULL) {
         return true;
     } else {
         out->priv = calloc(1, sizeof(private_data_t));
         if (out->priv == NULL) {
             return false;
         }
 
         priv = out->priv;
     }
 
     priv->parent_q = g_queue_new();
 
     priv->root = pcmk__xe_create(NULL, "html");
     xmlCreateIntSubset(priv->root->doc, (pcmkXmlStr) "html", NULL, NULL);
 
     crm_xml_add(priv->root, PCMK_XA_LANG, PCMK__VALUE_EN);
     g_queue_push_tail(priv->parent_q, priv->root);
     priv->errors = NULL;
 
     pcmk__output_xml_create_parent(out, "body", NULL);
 
     return true;
 }
 
 static void
 add_error_node(gpointer data, gpointer user_data) {
     char *str = (char *) data;
     pcmk__output_t *out = (pcmk__output_t *) user_data;
     out->list_item(out, NULL, "%s", str);
 }
 
 static void
 html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) {
     private_data_t *priv = NULL;
     htmlNodePtr head_node = NULL;
     htmlNodePtr charset_node = NULL;
     xmlNode *child_node = NULL;
 
     CRM_ASSERT(out != NULL);
 
     priv = out->priv;
 
     /* If root is NULL, html_init failed and we are being called from pcmk__output_free
      * in the pcmk__output_new path.
      */
     if (priv == NULL || priv->root == NULL) {
         return;
     }
 
     if (cgi_output && print) {
         fprintf(out->dest, "Content-Type: text/html\n\n");
     }
 
     /* Add the head node last - it's not needed earlier because it doesn't contain
      * anything else that the user could add, and we want it done last to pick up
      * any options that may have been given.
      */
     head_node = xmlNewDocRawNode(NULL, NULL, (pcmkXmlStr) "head", NULL);
 
     if (title != NULL ) {
         child_node = pcmk__xe_create(head_node, "title");
         pcmk__xe_set_content(child_node, "%s", title);
     } else if (out->request != NULL) {
         child_node = pcmk__xe_create(head_node, "title");
         pcmk__xe_set_content(child_node, "%s", out->request);
     }
 
     charset_node = pcmk__xe_create(head_node, PCMK__XE_META);
     crm_xml_add(charset_node, "charset", "utf-8");
 
     /* Add any extra header nodes the caller might have created. */
     for (int i = 0; i < g_slist_length(extra_headers); i++) {
         xmlAddChild(head_node, xmlCopyNode(g_slist_nth_data(extra_headers, i), 1));
     }
 
     /* Stylesheets are included two different ways.  The first is via a built-in
      * default (see the stylesheet_default const above).  The second is via the
      * html-stylesheet option, and this should obviously be a link to a
      * stylesheet.  The second can override the first.  At least one should be
      * given.
      */
     child_node = pcmk__xe_create(head_node, "style");
     pcmk__xe_set_content(child_node, "%s", stylesheet_default);
 
     if (stylesheet_link != NULL) {
         htmlNodePtr link_node = pcmk__xe_create(head_node, "link");
         pcmk__xe_set_props(link_node, "rel", "stylesheet",
                            "href", stylesheet_link,
                            NULL);
     }
 
     xmlAddPrevSibling(priv->root->children, head_node);
 
     if (g_slist_length(priv->errors) > 0) {
         out->begin_list(out, "Errors", NULL, NULL);
         g_slist_foreach(priv->errors, add_error_node, (gpointer) out);
         out->end_list(out);
     }
 
     if (print) {
         htmlDocDump(out->dest, priv->root->doc);
     }
 
     if (copy_dest != NULL) {
         *copy_dest = pcmk__xml_copy(NULL, priv->root);
     }
 
-    g_slist_free_full(extra_headers, (GDestroyNotify) xmlFreeNode);
+    g_slist_free_full(extra_headers, (GDestroyNotify) free_xml);
     extra_headers = NULL;
 }
 
 static void
 html_reset(pcmk__output_t *out) {
     CRM_ASSERT(out != NULL);
 
     out->dest = freopen(NULL, "w", out->dest);
     CRM_ASSERT(out->dest != NULL);
 
     html_free_priv(out);
     html_init(out);
 }
 
 static void
 html_subprocess_output(pcmk__output_t *out, int exit_status,
                        const char *proc_stdout, const char *proc_stderr) {
     char *rc_buf = NULL;
 
     CRM_ASSERT(out != NULL);
 
     rc_buf = crm_strdup_printf("Return code: %d", exit_status);
 
     pcmk__output_create_xml_text_node(out, "h2", "Command Output");
     pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, rc_buf);
 
     if (proc_stdout != NULL) {
         pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Stdout");
         pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL,
                                       PCMK__VALUE_OUTPUT, proc_stdout);
     }
     if (proc_stderr != NULL) {
         pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Stderr");
         pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL,
                                       PCMK__VALUE_OUTPUT, proc_stderr);
     }
 
     free(rc_buf);
 }
 
 static void
 html_version(pcmk__output_t *out, bool extended) {
     CRM_ASSERT(out != NULL);
 
     pcmk__output_create_xml_text_node(out, "h2", "Version Information");
     pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
                                   "Program: Pacemaker");
     pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
                                   "Version: " PACEMAKER_VERSION);
     pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
                                   "Author: Andrew Beekhof and "
                                   "the Pacemaker project contributors");
     pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
                                   "Build: " BUILD_VERSION);
     pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
                                   "Features: " CRM_FEATURES);
 }
 
 G_GNUC_PRINTF(2, 3)
 static void
 html_err(pcmk__output_t *out, const char *format, ...) {
     private_data_t *priv = NULL;
     int len = 0;
     char *buf = NULL;
     va_list ap;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
     va_start(ap, format);
     len = vasprintf(&buf, format, ap);
     CRM_ASSERT(len >= 0);
     va_end(ap);
 
     priv->errors = g_slist_append(priv->errors, buf);
 }
 
 G_GNUC_PRINTF(2, 3)
 static int
 html_info(pcmk__output_t *out, const char *format, ...) {
     return pcmk_rc_no_output;
 }
 
 static void
 html_output_xml(pcmk__output_t *out, const char *name, const char *buf) {
     htmlNodePtr node = NULL;
 
     CRM_ASSERT(out != NULL);
 
     node = pcmk__output_create_html_node(out, "pre", NULL, NULL, buf);
     crm_xml_add(node, PCMK_XA_LANG, "xml");
 }
 
 G_GNUC_PRINTF(4, 5)
 static void
 html_begin_list(pcmk__output_t *out, const char *singular_noun,
                 const char *plural_noun, const char *format, ...) {
     int q_len = 0;
     private_data_t *priv = NULL;
     xmlNodePtr node = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
     /* If we are already in a list (the queue depth is always at least
      * one because of the <html> element), first create a <li> element
      * to hold the <h2> and the new list.
      */
     q_len = g_queue_get_length(priv->parent_q);
     if (q_len > 2) {
         pcmk__output_xml_create_parent(out, "li", NULL);
     }
 
     if (format != NULL) {
         va_list ap;
         char *buf = NULL;
         int len;
 
         va_start(ap, format);
         len = vasprintf(&buf, format, ap);
         va_end(ap);
         CRM_ASSERT(len >= 0);
 
         if (q_len > 2) {
             pcmk__output_create_xml_text_node(out, "h3", buf);
         } else {
             pcmk__output_create_xml_text_node(out, "h2", buf);
         }
 
         free(buf);
     }
 
     node = pcmk__output_xml_create_parent(out, "ul", NULL);
     g_queue_push_tail(priv->parent_q, node);
 }
 
 G_GNUC_PRINTF(3, 4)
 static void
 html_list_item(pcmk__output_t *out, const char *name, const char *format, ...) {
     htmlNodePtr item_node = NULL;
     va_list ap;
     char *buf = NULL;
     int len;
 
     CRM_ASSERT(out != NULL);
 
     va_start(ap, format);
     len = vasprintf(&buf, format, ap);
     CRM_ASSERT(len >= 0);
     va_end(ap);
 
     item_node = pcmk__output_create_xml_text_node(out, "li", buf);
     free(buf);
 
     if (name != NULL) {
         crm_xml_add(item_node, PCMK_XA_CLASS, name);
     }
 }
 
 static void
 html_increment_list(pcmk__output_t *out) {
     /* This function intentially left blank */
 }
 
 static void
 html_end_list(pcmk__output_t *out) {
     private_data_t *priv = NULL;
 
     CRM_ASSERT(out != NULL && out->priv != NULL);
     priv = out->priv;
 
     /* Remove the <ul> tag, but do not free this result - it's still
      * part of the document.
      */
     g_queue_pop_tail(priv->parent_q);
     pcmk__output_xml_pop_parent(out);
 
     /* Remove the <li> created for nested lists. */
     if (g_queue_get_length(priv->parent_q) > 2) {
         pcmk__output_xml_pop_parent(out);
     }
 }
 
 static bool
 html_is_quiet(pcmk__output_t *out) {
     return false;
 }
 
 static void
 html_spacer(pcmk__output_t *out) {
     CRM_ASSERT(out != NULL);
     pcmk__output_create_xml_node(out, "br", NULL);
 }
 
 static void
 html_progress(pcmk__output_t *out, bool end) {
     /* This function intentially left blank */
 }
 
 pcmk__output_t *
 pcmk__mk_html_output(char **argv) {
     pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
 
     if (retval == NULL) {
         return NULL;
     }
 
     retval->fmt_name = "html";
     retval->request = pcmk__quote_cmdline(argv);
 
     retval->init = html_init;
     retval->free_priv = html_free_priv;
     retval->finish = html_finish;
     retval->reset = html_reset;
 
     retval->register_message = pcmk__register_message;
     retval->message = pcmk__call_message;
 
     retval->subprocess_output = html_subprocess_output;
     retval->version = html_version;
     retval->info = html_info;
     retval->transient = html_info;
     retval->err = html_err;
     retval->output_xml = html_output_xml;
 
     retval->begin_list = html_begin_list;
     retval->list_item = html_list_item;
     retval->increment_list = html_increment_list;
     retval->end_list = html_end_list;
 
     retval->is_quiet = html_is_quiet;
     retval->spacer = html_spacer;
     retval->progress = html_progress;
     retval->prompt = pcmk__text_prompt;
 
     return retval;
 }
 
 xmlNodePtr
 pcmk__output_create_html_node(pcmk__output_t *out, const char *element_name, const char *id,
                               const char *class_name, const char *text) {
     htmlNodePtr node = NULL;
 
     CRM_ASSERT(out != NULL);
     CRM_CHECK(pcmk__str_eq(out->fmt_name, "html", pcmk__str_none), return NULL);
 
     node = pcmk__output_create_xml_text_node(out, element_name, text);
 
     if (class_name != NULL) {
         crm_xml_add(node, PCMK_XA_CLASS, class_name);
     }
 
     if (id != NULL) {
         crm_xml_add(node, PCMK_XA_ID, id);
     }
 
     return node;
 }
 
 /*!
  * \internal
  * \brief Create a new HTML element under a given parent with ID and class
  *
  * \param[in,out] parent  XML element that will be the new element's parent
  *                        (\c NULL to create a new XML document with the new
  *                        node as root)
  * \param[in]     name    Name of new element
  * \param[in]     id      CSS ID of new element (can be \c NULL)
  * \param[in]     class   CSS class of new element (can be \c NULL)
  *
  * \return Newly created XML element (guaranteed not to be \c NULL)
  */
 xmlNode *
 pcmk__html_create(xmlNode *parent, const char *name, const char *id,
                   const char *class)
 {
     xmlNode *node = pcmk__xe_create(parent, name);
 
     pcmk__xe_set_props(node,
                        PCMK_XA_CLASS, class,
                        PCMK_XA_ID, id,
                        NULL);
     return node;
 }
 
 void
 pcmk__html_add_header(const char *name, ...) {
     htmlNodePtr header_node;
     va_list ap;
 
     va_start(ap, name);
 
     header_node = xmlNewDocRawNode(NULL, NULL, (pcmkXmlStr) name, NULL);
     while (1) {
         char *key = va_arg(ap, char *);
         char *value;
 
         if (key == NULL) {
             break;
         }
 
         value = va_arg(ap, char *);
         crm_xml_add(header_node, key, value);
     }
 
     extra_headers = g_slist_append(extra_headers, header_node);
 
     va_end(ap);
 }
diff --git a/lib/common/tests/xml/crm_xml_init_test.c b/lib/common/tests/xml/crm_xml_init_test.c
index f9f65120a4..3e020f2172 100644
--- a/lib/common/tests/xml/crm_xml_init_test.c
+++ b/lib/common/tests/xml/crm_xml_init_test.c
@@ -1,226 +1,226 @@
 /*
  * Copyright 2023-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 <crm/common/xml.h>
 #include <crm/common/unittest_internal.h>
 #include <crm/common/xml_internal.h>
 
 #include "crmcommon_private.h"
 
 static int
 setup(void **state) {
     crm_xml_init();
     return 0;
 }
 
 static int
 teardown(void **state) {
     crm_xml_cleanup();
     return 0;
 }
 
 static void
 buffer_scheme_test(void **state) {
     assert_int_equal(XML_BUFFER_ALLOC_DOUBLEIT, xmlGetBufferAllocationScheme());
 }
 
 /* These functions also serve as unit tests of the static new_private_data
  * function.  We can't test free_private_data because libxml will call that as
  * part of freeing everything else.  By the time we'd get back into a unit test
  * where we could check that private members are NULL, the structure containing
  * the private data would have been freed.
  *
  * This could probably be tested with a lot of function mocking, but that
  * doesn't seem worth it.
  */
 
 static void
 create_document_node(void **state) {
     xml_doc_private_t *docpriv = NULL;
     xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
 
     /* Double check things */
     assert_non_null(doc);
     assert_int_equal(doc->type, XML_DOCUMENT_NODE);
 
     /* Check that the private data is initialized correctly */
     docpriv = doc->_private;
     assert_non_null(docpriv);
     assert_int_equal(docpriv->check, PCMK__XML_DOC_PRIVATE_MAGIC);
     assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty|pcmk__xf_created));
 
     /* Clean up */
     xmlFreeDoc(doc);
 }
 
 static void
 create_element_node(void **state) {
     xml_doc_private_t *docpriv = NULL;
     xml_node_private_t *priv = NULL;
     xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
     xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL);
 
     /* Adding a node to the document marks it as dirty */
     docpriv = doc->_private;
     assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
 
     /* Double check things */
     assert_non_null(node);
     assert_int_equal(node->type, XML_ELEMENT_NODE);
 
     /* Check that the private data is initialized correctly */
     priv = node->_private;
     assert_non_null(priv);
     assert_int_equal(priv->check, PCMK__XML_NODE_PRIVATE_MAGIC);
     assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
 
     /* Clean up */
-    xmlFreeNode(node);
+    free_xml(node);
     xmlFreeDoc(doc);
 }
 
 static void
 create_attr_node(void **state) {
     xml_doc_private_t *docpriv = NULL;
     xml_node_private_t *priv = NULL;
     xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
     xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL);
     xmlAttrPtr attr = xmlNewProp(node, (pcmkXmlStr) PCMK_XA_NAME,
                                  (pcmkXmlStr) "dummy-value");
 
     /* Adding a node to the document marks it as dirty */
     docpriv = doc->_private;
     assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
 
     /* Double check things */
     assert_non_null(attr);
     assert_int_equal(attr->type, XML_ATTRIBUTE_NODE);
 
     /* Check that the private data is initialized correctly */
     priv = attr->_private;
     assert_non_null(priv);
     assert_int_equal(priv->check, PCMK__XML_NODE_PRIVATE_MAGIC);
     assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
 
     /* Clean up */
-    xmlFreeNode(node);
+    free_xml(node);
     xmlFreeDoc(doc);
 }
 
 static void
 create_comment_node(void **state) {
     xml_doc_private_t *docpriv = NULL;
     xml_node_private_t *priv = NULL;
     xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
     xmlNodePtr node = xmlNewDocComment(doc, (pcmkXmlStr) "blahblah");
 
     /* Adding a node to the document marks it as dirty */
     docpriv = doc->_private;
     assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
 
     /* Double check things */
     assert_non_null(node);
     assert_int_equal(node->type, XML_COMMENT_NODE);
 
     /* Check that the private data is initialized correctly */
     priv = node->_private;
     assert_non_null(priv);
     assert_int_equal(priv->check, PCMK__XML_NODE_PRIVATE_MAGIC);
     assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
 
     /* Clean up */
-    xmlFreeNode(node);
+    free_xml(node);
     xmlFreeDoc(doc);
 }
 
 static void
 create_text_node(void **state) {
     xml_doc_private_t *docpriv = NULL;
     xml_node_private_t *priv = NULL;
     xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
     xmlNodePtr node = xmlNewDocText(doc, (pcmkXmlStr) "blahblah");
 
     /* Adding a node to the document marks it as dirty */
     docpriv = doc->_private;
     assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
 
     /* Double check things */
     assert_non_null(node);
     assert_int_equal(node->type, XML_TEXT_NODE);
 
     /* Check that no private data was created */
     priv = node->_private;
     assert_null(priv);
 
     /* Clean up */
-    xmlFreeNode(node);
+    free_xml(node);
     xmlFreeDoc(doc);
 }
 
 static void
 create_dtd_node(void **state) {
     xml_doc_private_t *docpriv = NULL;
     xml_node_private_t *priv = NULL;
     xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
     xmlDtdPtr dtd = xmlNewDtd(doc, (pcmkXmlStr) PCMK_XA_NAME,
                               (pcmkXmlStr) "externalId",
                               (pcmkXmlStr) "systemId");
 
     /* Adding a node to the document marks it as dirty */
     docpriv = doc->_private;
     assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
 
     /* Double check things */
     assert_non_null(dtd);
     assert_int_equal(dtd->type, XML_DTD_NODE);
 
     /* Check that no private data was created */
     priv = dtd->_private;
     assert_null(priv);
 
     /* Clean up */
     /* If you call xmlFreeDtd before xmlFreeDoc, you get a segfault */
     xmlFreeDoc(doc);
 }
 
 static void
 create_cdata_node(void **state) {
     xml_doc_private_t *docpriv = NULL;
     xml_node_private_t *priv = NULL;
     xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
     xmlNodePtr node = xmlNewCDataBlock(doc, (pcmkXmlStr) "blahblah", 8);
 
     /* Adding a node to the document marks it as dirty */
     docpriv = doc->_private;
     assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
 
     /* Double check things */
     assert_non_null(node);
     assert_int_equal(node->type, XML_CDATA_SECTION_NODE);
 
     /* Check that no private data was created */
     priv = node->_private;
     assert_null(priv);
 
     /* Clean up */
-    xmlFreeNode(node);
+    free_xml(node);
     xmlFreeDoc(doc);
 }
 
 PCMK__UNIT_TEST(setup, teardown,
                 cmocka_unit_test(buffer_scheme_test),
                 cmocka_unit_test(create_document_node),
                 cmocka_unit_test(create_element_node),
                 cmocka_unit_test(create_attr_node),
                 cmocka_unit_test(create_comment_node),
                 cmocka_unit_test(create_text_node),
                 cmocka_unit_test(create_dtd_node),
                 cmocka_unit_test(create_cdata_node));
diff --git a/lib/common/xml.c b/lib/common/xml.c
index 581da83174..ee2cf12a7f 100644
--- a/lib/common/xml.c
+++ b/lib/common/xml.c
@@ -1,2722 +1,2725 @@
 /*
  * 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 Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdarg.h>
 #include <stdint.h>                     // uint32_t
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/stat.h>                   // stat(), S_ISREG, etc.
 #include <sys/types.h>
 
 #include <libxml/parser.h>
 #include <libxml/tree.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>    // PCMK__XML_LOG_BASE, etc.
 #include "crmcommon_private.h"
 
 /*!
  * \internal
  * \brief Apply a function to each XML node in a tree (pre-order, depth-first)
  *
  * \param[in,out] xml        XML tree to traverse
  * \param[in,out] fn         Function to call for each node (returns \c true to
  *                           continue traversing the tree or \c false to stop)
  * \param[in,out] user_data  Argument to \p fn
  *
  * \return \c false if any \p fn call returned \c false, or \c true otherwise
  *
  * \note This function is recursive.
  */
 bool
 pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *),
                        void *user_data)
 {
     if (!fn(xml, user_data)) {
         return false;
     }
 
     for (xml = pcmk__xml_first_child(xml); xml != NULL;
          xml = pcmk__xml_next(xml)) {
 
         if (!pcmk__xml_tree_foreach(xml, fn, user_data)) {
             return false;
         }
     }
     return true;
 }
 
 bool
 pcmk__tracking_xml_changes(xmlNode *xml, bool lazy)
 {
     if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) {
         return FALSE;
     } else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags,
                             pcmk__xf_tracking)) {
         return FALSE;
     } else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags,
                                     pcmk__xf_lazy)) {
         return FALSE;
     }
     return TRUE;
 }
 
 static inline void
 set_parent_flag(xmlNode *xml, long flag) 
 {
     for(; xml; xml = xml->parent) {
         xml_node_private_t *nodepriv = xml->_private;
 
         if (nodepriv == NULL) {
             /* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */
         } else {
             pcmk__set_xml_flags(nodepriv, flag);
         }
     }
 }
 
 void
 pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag)
 {
     if(xml && xml->doc && xml->doc->_private){
         /* During calls to xmlDocCopyNode(), xml->doc may be unset */
         xml_doc_private_t *docpriv = xml->doc->_private;
 
         pcmk__set_xml_flags(docpriv, flag);
     }
 }
 
 // Mark document, element, and all element's parents as changed
 void
 pcmk__mark_xml_node_dirty(xmlNode *xml)
 {
     pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty);
     set_parent_flag(xml, pcmk__xf_dirty);
 }
 
 /*!
  * \internal
  * \brief Clear flags on an XML node
  *
  * \param[in,out] xml        XML node whose flags to reset
  * \param[in,out] user_data  Ignored
  *
  * \return \c true (to continue traversing the tree)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 reset_xml_node_flags(xmlNode *xml, void *user_data)
 {
     xml_node_private_t *nodepriv = xml->_private;
 
     if (nodepriv != NULL) {
         nodepriv->flags = pcmk__xf_none;
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node
  *
  * \param[in,out] xml        Node whose flags to set
  * \param[in]     user_data  Ignored
  *
  * \return \c true (to continue traversing the tree)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 mark_xml_dirty_created(xmlNode *xml, void *user_data)
 {
     xml_node_private_t *nodepriv = xml->_private;
 
     if (nodepriv != NULL) {
         pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
     }
     return true;
 }
 
 /*!
  * \internal
  * \brief Mark an XML tree as dirty and created, and mark its parents dirty
  *
  * Also mark the document dirty.
  *
  * \param[in,out] xml  Tree to mark as dirty and created
  */
 void
 pcmk__xml_mark_created(xmlNode *xml)
 {
     CRM_ASSERT(xml != NULL);
 
     if (!pcmk__tracking_xml_changes(xml, false)) {
         // Tracking is disabled for entire document
         return;
     }
 
     // Mark all parents and document dirty
     pcmk__mark_xml_node_dirty(xml);
 
     pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL);
 }
 
 // Free an XML object previously marked as deleted
 static void
 free_deleted_object(void *data)
 {
     if(data) {
         pcmk__deleted_xml_t *deleted_obj = data;
 
         g_free(deleted_obj->path);
         free(deleted_obj);
     }
 }
 
 // Free and NULL user, ACLs, and deleted objects in an XML node's private data
 static void
 reset_xml_private_data(xml_doc_private_t *docpriv)
 {
     if (docpriv != NULL) {
         CRM_ASSERT(docpriv->check == PCMK__XML_DOC_PRIVATE_MAGIC);
 
         free(docpriv->user);
         docpriv->user = NULL;
 
         if (docpriv->acls != NULL) {
             pcmk__free_acls(docpriv->acls);
             docpriv->acls = NULL;
         }
 
         if(docpriv->deleted_objs) {
             g_list_free_full(docpriv->deleted_objs, free_deleted_object);
             docpriv->deleted_objs = NULL;
         }
     }
 }
 
 // Free all private data associated with an XML node
 static void
 free_private_data(xmlNode *node)
 {
     /* Note:
     
     This function frees private data assosciated with an XML node,
     unless the function is being called as a result of internal
     XSLT cleanup.
     
     That could happen through, for example, the following chain of
     function calls:
     
        xsltApplyStylesheetInternal
     -> xsltFreeTransformContext
     -> xsltFreeRVTs
     -> xmlFreeDoc
 
     And in that case, the node would fulfill three conditions:
     
     1. It would be a standalone document (i.e. it wouldn't be 
        part of a document)
     2. It would have a space-prefixed name (for reference, please
        see xsltInternals.h: XSLT_MARK_RES_TREE_FRAG)
     3. It would carry its own payload in the _private field.
     
     We do not free data in this circumstance to avoid a failed
     assertion on the PCMK__XML_*_PRIVATE_MAGIC later.
     
     */
     if (node->name == NULL || node->name[0] != ' ') {
         if (node->_private) {
             if (node->type == XML_DOCUMENT_NODE) {
                 reset_xml_private_data(node->_private);
             } else {
                 CRM_ASSERT(((xml_node_private_t *) node->_private)->check
                                == PCMK__XML_NODE_PRIVATE_MAGIC);
                 /* nothing dynamically allocated nested */
             }
             free(node->_private);
             node->_private = NULL;
         }
     }
 }
 
 // Allocate and initialize private data for an XML node
 static void
 new_private_data(xmlNode *node)
 {
     switch (node->type) {
         case XML_DOCUMENT_NODE: {
             xml_doc_private_t *docpriv =
                 pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
 
             docpriv->check = PCMK__XML_DOC_PRIVATE_MAGIC;
             /* Flags will be reset if necessary when tracking is enabled */
             pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created);
             node->_private = docpriv;
             break;
         }
         case XML_ELEMENT_NODE:
         case XML_ATTRIBUTE_NODE:
         case XML_COMMENT_NODE: {
             xml_node_private_t *nodepriv =
                 pcmk__assert_alloc(1, sizeof(xml_node_private_t));
 
             nodepriv->check = PCMK__XML_NODE_PRIVATE_MAGIC;
             /* Flags will be reset if necessary when tracking is enabled */
             pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
             node->_private = nodepriv;
             if (pcmk__tracking_xml_changes(node, FALSE)) {
                 /* XML_ELEMENT_NODE doesn't get picked up here, node->doc is
                  * not hooked up at the point we are called
                  */
                 pcmk__mark_xml_node_dirty(node);
             }
             break;
         }
         case XML_TEXT_NODE:
         case XML_DTD_NODE:
         case XML_CDATA_SECTION_NODE:
             break;
         default:
             /* Ignore */
             crm_trace("Ignoring %p %d", node, node->type);
             CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE);
             break;
     }
 }
 
 void
 xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls) 
 {
     xml_accept_changes(xml);
     crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml);
     pcmk__set_xml_doc_flag(xml, pcmk__xf_tracking);
     if(enforce_acls) {
         if(acl_source == NULL) {
             acl_source = xml;
         }
         pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_enabled);
         pcmk__unpack_acl(acl_source, xml, user);
         pcmk__apply_acl(xml);
     }
 }
 
 bool xml_tracking_changes(xmlNode * xml)
 {
     return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL)
            && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags,
                           pcmk__xf_tracking);
 }
 
 bool xml_document_dirty(xmlNode *xml) 
 {
     return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL)
            && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags,
                           pcmk__xf_dirty);
 }
 
 /*!
  * \internal
  * \brief Return ordinal position of an XML node among its siblings
  *
  * \param[in] xml            XML node to check
  * \param[in] ignore_if_set  Don't count siblings with this flag set
  *
  * \return Ordinal position of \p xml (starting with 0)
  */
 int
 pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set)
 {
     int position = 0;
 
     for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) {
         xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private;
 
         if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) {
             position++;
         }
     }
 
     return position;
 }
 
 /*!
  * \internal
  * \brief Remove all attributes marked as deleted from an XML node
  *
  * \param[in,out] xml        XML node whose deleted attributes to remove
  * \param[in,out] user_data  Ignored
  *
  * \return \c true (to continue traversing the tree)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 accept_attr_deletions(xmlNode *xml, void *user_data)
 {
     reset_xml_node_flags(xml, NULL);
     pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL);
     return true;
 }
 
 /*!
  * \internal
  * \brief Find first child XML node matching another given XML node
  *
  * \param[in] haystack  XML whose children should be checked
  * \param[in] needle    XML to match (comment content or element name and ID)
  * \param[in] exact     If true and needle is a comment, position must match
  */
 xmlNode *
 pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact)
 {
     CRM_CHECK(needle != NULL, return NULL);
 
     if (needle->type == XML_COMMENT_NODE) {
         return pcmk__xc_match(haystack, needle, exact);
 
     } else {
         const char *id = pcmk__xe_id(needle);
         const char *attr = (id == NULL)? NULL : PCMK_XA_ID;
 
         return pcmk__xe_first_child(haystack, (const char *) needle->name, attr,
                                     id);
     }
 }
 
 void
 xml_accept_changes(xmlNode * xml)
 {
     xmlNode *top = NULL;
     xml_doc_private_t *docpriv = NULL;
 
     if(xml == NULL) {
         return;
     }
 
     crm_trace("Accepting changes to %p", xml);
     docpriv = xml->doc->_private;
     top = xmlDocGetRootElement(xml->doc);
 
     reset_xml_private_data(xml->doc->_private);
 
     if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) {
         docpriv->flags = pcmk__xf_none;
         return;
     }
 
     docpriv->flags = pcmk__xf_none;
     pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL);
 }
 
 /*!
  * \internal
  * \brief Find first XML child element matching given criteria
  *
  * \param[in] parent     XML element to search (can be \c NULL)
  * \param[in] node_name  If not \c NULL, only match children of this type
  * \param[in] attr_n     If not \c NULL, only match children with an attribute
  *                       of this name.
  * \param[in] attr_v     If \p attr_n and this are not NULL, only match children
  *                       with an attribute named \p attr_n and this value
  *
  * \return Matching XML child element, or \c NULL if none found
  */
 xmlNode *
 pcmk__xe_first_child(const xmlNode *parent, const char *node_name,
                      const char *attr_n, const char *attr_v)
 {
     xmlNode *child = NULL;
     const char *parent_name = "<null>";
 
     CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL);
 
     if (parent != NULL) {
         child = parent->children;
         while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) {
             child = child->next;
         }
 
         parent_name = (const char *) parent->name;
     }
 
     for (; child != NULL; child = pcmk__xe_next(child)) {
         const char *value = NULL;
 
         if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) {
             // Node name mismatch
             continue;
         }
         if (attr_n == NULL) {
             // No attribute match needed
             return child;
         }
 
         value = crm_element_value(child, attr_n);
 
         if ((attr_v == NULL) && (value != NULL)) {
             // attr_v == NULL: Attribute attr_n must be set (to any value)
             return child;
         }
         if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) {
             // attr_v != NULL: Attribute attr_n must be set to value attr_v
             return child;
         }
     }
 
     if (node_name == NULL) {
         node_name = "(any)";    // For logging
     }
     if (attr_n != NULL) {
         crm_trace("XML child node <%s %s=%s> not found in %s",
                   node_name, attr_n, attr_v, parent_name);
     } else {
         crm_trace("XML child node <%s> not found in %s",
                   node_name, parent_name);
     }
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Set an XML attribute, expanding \c ++ and \c += where appropriate
  *
  * If \p target already has an attribute named \p name set to an integer value
  * and \p value is an addition assignment expression on \p name, then expand
  * \p value to an integer and set attribute \p name to the expanded value in
  * \p target.
  *
  * Otherwise, set attribute \p name on \p target using the literal \p value.
  *
  * The original attribute value in \p target and the number in an assignment
  * expression in \p value are parsed and added as scores (that is, their values
  * are capped at \c INFINITY and \c -INFINITY). For more details, refer to
  * \c char2score().
  *
  * For example, suppose \p target has an attribute named \c "X" with value
  * \c "5", and that \p name is \c "X".
  * * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6".
  * * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8".
  * * If \p value is \c "val", the new value of \c "X" in \p target is \c "val".
  * * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++".
  *
  * \param[in,out] target  XML node whose attribute to set
  * \param[in]     name    Name of the attribute to set
  * \param[in]     value   New value of attribute to set
  *
  * \return Standard Pacemaker return code (specifically, \c EINVAL on invalid
  *         argument, or \c pcmk_rc_ok otherwise)
  */
 int
 pcmk__xe_set_score(xmlNode *target, const char *name, const char *value)
 {
     const char *old_value = NULL;
 
     CRM_CHECK((target != NULL) && (name != NULL), return EINVAL);
 
     if (value == NULL) {
         return pcmk_rc_ok;
     }
 
     old_value = crm_element_value(target, name);
 
     // If no previous value, skip to default case and set the value unexpanded.
     if (old_value != NULL) {
         const char *n = name;
         const char *v = value;
 
         // Stop at first character that differs between name and value
         for (; (*n == *v) && (*n != '\0'); n++, v++);
 
         // If value begins with name followed by a "++" or "+="
         if ((*n == '\0')
             && (*v++ == '+')
             && ((*v == '+') || (*v == '='))) {
 
             // If we're expanding ourselves, no previous value was set; use 0
             int old_value_i = (old_value != value)? char2score(old_value) : 0;
 
             /* value="X++": new value of X is old_value + 1
              * value="X+=Y": new value of X is old_value + Y (for some number Y)
              */
             int add = (*v == '+')? 1 : char2score(++v);
 
             crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add));
             return pcmk_rc_ok;
         }
     }
 
     // Default case: set the attribute unexpanded (with value treated literally)
     if (old_value != value) {
         crm_xml_add(target, name, value);
     }
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Copy XML attributes from a source element to a target element
  *
  * This is similar to \c xmlCopyPropList() except that attributes are marked
  * as dirty for change tracking purposes.
  *
  * \param[in,out] target  XML element to receive copied attributes from \p src
  * \param[in]     src     XML element whose attributes to copy to \p target
  * \param[in]     flags   Group of <tt>enum pcmk__xa_flags</tt>
  *
  * \return Standard Pacemaker return code
  */
 int
 pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags)
 {
     CRM_CHECK((src != NULL) && (target != NULL), return EINVAL);
 
     for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL;
          attr = attr->next) {
 
         const char *name = (const char *) attr->name;
         const char *value = pcmk__xml_attr_value(attr);
 
         if (pcmk_is_set(flags, pcmk__xaf_no_overwrite)
             && (crm_element_value(target, name) != NULL)) {
             continue;
         }
 
         if (pcmk_is_set(flags, pcmk__xaf_score_update)) {
             pcmk__xe_set_score(target, name, value);
         } else {
             crm_xml_add(target, name, value);
         }
     }
 
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Remove an XML attribute from an element
  *
  * \param[in,out] element  XML element that owns \p attr
  * \param[in,out] attr     XML attribute to remove from \p element
  *
  * \return Standard Pacemaker return code (\c EPERM if ACLs prevent removal of
  *         attributes from \p element, or \c pcmk_rc_ok otherwise)
  */
 static int
 remove_xe_attr(xmlNode *element, xmlAttr *attr)
 {
     if (attr == NULL) {
         return pcmk_rc_ok;
     }
 
     if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) {
         // ACLs apply to element, not to particular attributes
         crm_trace("ACLs prevent removal of attributes from %s element",
                   (const char *) element->name);
         return EPERM;
     }
 
     if (pcmk__tracking_xml_changes(element, false)) {
         // Leave in place (marked for removal) until after diff is calculated
         set_parent_flag(element, pcmk__xf_dirty);
         pcmk__set_xml_flags((xml_node_private_t *) attr->_private,
                             pcmk__xf_deleted);
     } else {
         xmlRemoveProp(attr);
     }
     return pcmk_rc_ok;
 }
 
 /*!
  * \internal
  * \brief Remove a named attribute from an XML element
  *
  * \param[in,out] element  XML element to remove an attribute from
  * \param[in]     name     Name of attribute to remove
  */
 void
 pcmk__xe_remove_attr(xmlNode *element, const char *name)
 {
     if (name != NULL) {
         remove_xe_attr(element, xmlHasProp(element, (pcmkXmlStr) name));
     }
 }
 
 /*!
  * \internal
  * \brief Remove a named attribute from an XML element
  *
  * This is a wrapper for \c pcmk__xe_remove_attr() for use with
  * \c pcmk__xml_tree_foreach().
  *
  * \param[in,out] xml        XML element to remove an attribute from
  * \param[in]     user_data  Name of attribute to remove
  *
  * \return \c true (to continue traversing the tree)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 bool
 pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data)
 {
     const char *name = user_data;
 
     pcmk__xe_remove_attr(xml, name);
     return true;
 }
 
 /*!
  * \internal
  * \brief Remove an XML element's attributes that match some criteria
  *
  * \param[in,out] element    XML element to modify
  * \param[in]     match      If not NULL, only remove attributes for which
  *                           this function returns true
  * \param[in,out] user_data  Data to pass to \p match
  */
 void
 pcmk__xe_remove_matching_attrs(xmlNode *element,
                                bool (*match)(xmlAttrPtr, void *),
                                void *user_data)
 {
     xmlAttrPtr next = NULL;
 
     for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) {
         next = a->next; // Grab now because attribute might get removed
         if ((match == NULL) || match(a, user_data)) {
             if (remove_xe_attr(element, a) != pcmk_rc_ok) {
                 return;
             }
         }
     }
 }
 
 /*!
  * \internal
  * \brief Create a new XML element under a given parent
  *
  * \param[in,out] parent  XML element that will be the new element's parent
  *                        (\c NULL to create a new XML document with the new
  *                        node as root)
  * \param[in]     name    Name of new element
  *
  * \return Newly created XML element (guaranteed not to be \c NULL)
  */
 xmlNode *
 pcmk__xe_create(xmlNode *parent, const char *name)
 {
     xmlNode *node = NULL;
 
     CRM_ASSERT(!pcmk__str_empty(name));
 
     if (parent == NULL) {
         xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION);
 
         pcmk__mem_assert(doc);
 
         node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL);
         pcmk__mem_assert(node);
 
         xmlDocSetRootElement(doc, node);
 
     } else {
         node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL);
         pcmk__mem_assert(node);
     }
 
     pcmk__xml_mark_created(node);
     return node;
 }
 
 /*!
  * \internal
  * \brief Set a formatted string as an XML node's content
  *
  * \param[in,out] node    Node whose content to set
  * \param[in]     format  <tt>printf(3)</tt>-style format string
  * \param[in]     ...     Arguments for \p format
  *
  * \note This function escapes special characters. \c xmlNodeSetContent() does
  *       not.
  */
 G_GNUC_PRINTF(2, 3)
 void
 pcmk__xe_set_content(xmlNode *node, const char *format, ...)
 {
     if (node != NULL) {
         const char *content = NULL;
         char *buf = NULL;
 
         if (strchr(format, '%') == NULL) {
             // Nothing to format
             content = format;
 
         } else {
             va_list ap;
 
             va_start(ap, format);
 
             if (pcmk__str_eq(format, "%s", pcmk__str_none)) {
                 // No need to make a copy
                 content = va_arg(ap, const char *);
 
             } else {
                 CRM_ASSERT(vasprintf(&buf, format, ap) >= 0);
                 content = buf;
             }
             va_end(ap);
         }
 
         xmlNodeSetContent(node, (pcmkXmlStr) content);
         free(buf);
     }
 }
 
 /*!
  * Free an XML element and all of its children, removing it from its parent
  *
  * \param[in,out] xml  XML element to free
  */
 void
 pcmk_free_xml_subtree(xmlNode *xml)
 {
+    /* @TODO Free tree private data here when we drop
+     * new_private_data()/free_private_data()
+     */
     xmlUnlinkNode(xml); // Detaches from parent and siblings
     xmlFreeNode(xml);   // Frees
 }
 
 static void
 free_xml_with_position(xmlNode *child, int position)
 {
     xmlDoc *doc = NULL;
     xml_node_private_t *nodepriv = NULL;
 
     if (child == NULL) {
         return;
     }
     doc = child->doc;
     nodepriv = child->_private;
 
     if ((doc != NULL) && (xmlDocGetRootElement(doc) == child)) {
         // Free everything
         xmlFreeDoc(doc);
         return;
     }
 
     if (!pcmk__check_acl(child, NULL, pcmk__xf_acl_write)) {
         GString *xpath = NULL;
 
         pcmk__if_tracing({}, return);
         xpath = pcmk__element_xpath(child);
         qb_log_from_external_source(__func__, __FILE__,
                                     "Cannot remove %s %x", LOG_TRACE,
                                     __LINE__, 0, xpath->str, nodepriv->flags);
         g_string_free(xpath, TRUE);
         return;
     }
 
     if ((doc != NULL) && pcmk__tracking_xml_changes(child, false)
         && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
 
         xml_doc_private_t *docpriv = doc->_private;
         GString *xpath = pcmk__element_xpath(child);
 
         if (xpath != NULL) {
             pcmk__deleted_xml_t *deleted_obj = NULL;
 
             crm_trace("Deleting %s %p from %p", xpath->str, child, doc);
 
             deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t));
             deleted_obj->path = g_string_free(xpath, FALSE);
             deleted_obj->position = -1;
 
             // Record the position only for XML comments for now
             if (child->type == XML_COMMENT_NODE) {
                 if (position >= 0) {
                     deleted_obj->position = position;
 
                 } else {
                     deleted_obj->position = pcmk__xml_position(child,
                                                                pcmk__xf_skip);
                 }
             }
 
             docpriv->deleted_objs = g_list_append(docpriv->deleted_objs,
                                                   deleted_obj);
             pcmk__set_xml_doc_flag(child, pcmk__xf_dirty);
         }
     }
     pcmk_free_xml_subtree(child);
 }
 
 
 void
 free_xml(xmlNode * child)
 {
     free_xml_with_position(child, -1);
 }
 
 /*!
  * \internal
  * \brief Make a deep copy of an XML node under a given parent
  *
  * \param[in,out] parent  XML element that will be the copy's parent (\c NULL
  *                        to create a new XML document with the copy as root)
  * \param[in]     src     XML node to copy
  *
  * \return Deep copy of \p src, or \c NULL if \p src is \c NULL
  */
 xmlNode *
 pcmk__xml_copy(xmlNode *parent, xmlNode *src)
 {
     xmlNode *copy = NULL;
 
     if (src == NULL) {
         return NULL;
     }
 
     if (parent == NULL) {
         xmlDoc *doc = NULL;
 
         // The copy will be the root element of a new document
         CRM_ASSERT(src->type == XML_ELEMENT_NODE);
 
         doc = xmlNewDoc(PCMK__XML_VERSION);
         pcmk__mem_assert(doc);
 
         copy = xmlDocCopyNode(src, doc, 1);
         pcmk__mem_assert(copy);
 
         xmlDocSetRootElement(doc, copy);
 
     } else {
         copy = xmlDocCopyNode(src, parent->doc, 1);
         pcmk__mem_assert(copy);
 
         xmlAddChild(parent, copy);
     }
 
     pcmk__xml_mark_created(copy);
     return copy;
 }
 
 /*!
  * \internal
  * \brief Remove XML text nodes from specified XML and all its children
  *
  * \param[in,out] xml  XML to strip text from
  */
 void
 pcmk__strip_xml_text(xmlNode *xml)
 {
     xmlNode *iter = xml->children;
 
     while (iter) {
         xmlNode *next = iter->next;
 
         switch (iter->type) {
             case XML_TEXT_NODE:
                 /* Remove it */
                 pcmk_free_xml_subtree(iter);
                 break;
 
             case XML_ELEMENT_NODE:
                 /* Search it */
                 pcmk__strip_xml_text(iter);
                 break;
 
             default:
                 /* Leave it */
                 break;
         }
 
         iter = next;
     }
 }
 
 /*!
  * \internal
  * \brief Add a "last written" attribute to an XML element, set to current time
  *
  * \param[in,out] xe  XML element to add attribute to
  *
  * \return Value that was set, or NULL on error
  */
 const char *
 pcmk__xe_add_last_written(xmlNode *xe)
 {
     char *now_s = pcmk__epoch2str(NULL, 0);
     const char *result = NULL;
 
     result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN,
                          pcmk__s(now_s, "Could not determine current time"));
     free(now_s);
     return result;
 }
 
 /*!
  * \brief Sanitize a string so it is usable as an XML ID
  *
  * \param[in,out] id  String to sanitize
  */
 void
 crm_xml_sanitize_id(char *id)
 {
     char *c;
 
     for (c = id; *c; ++c) {
         /* @TODO Sanitize more comprehensively */
         switch (*c) {
             case ':':
             case '#':
                 *c = '.';
         }
     }
 }
 
 /*!
  * \brief Set the ID of an XML element using a format
  *
  * \param[in,out] xml  XML element
  * \param[in]     fmt  printf-style format
  * \param[in]     ...  any arguments required by format
  */
 void
 crm_xml_set_id(xmlNode *xml, const char *format, ...)
 {
     va_list ap;
     int len = 0;
     char *id = NULL;
 
     /* equivalent to crm_strdup_printf() */
     va_start(ap, format);
     len = vasprintf(&id, format, ap);
     va_end(ap);
     CRM_ASSERT(len > 0);
 
     crm_xml_sanitize_id(id);
     crm_xml_add(xml, PCMK_XA_ID, id);
     free(id);
 }
 
 /*!
  * \internal
  * \brief Check whether a string has XML special characters that must be escaped
  *
  * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details.
  *
  * \param[in] text  String to check
  * \param[in] type  Type of escaping
  *
  * \return \c true if \p text has special characters that need to be escaped, or
  *         \c false otherwise
  */
 bool
 pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type)
 {
     if (text == NULL) {
         return false;
     }
 
     while (*text != '\0') {
         switch (type) {
             case pcmk__xml_escape_text:
                 switch (*text) {
                     case '<':
                     case '>':
                     case '&':
                         return true;
                     case '\n':
                     case '\t':
                         break;
                     default:
                         if (g_ascii_iscntrl(*text)) {
                             return true;
                         }
                         break;
                 }
                 break;
 
             case pcmk__xml_escape_attr:
                 switch (*text) {
                     case '<':
                     case '>':
                     case '&':
                     case '"':
                         return true;
                     default:
                         if (g_ascii_iscntrl(*text)) {
                             return true;
                         }
                         break;
                 }
                 break;
 
             case pcmk__xml_escape_attr_pretty:
                 switch (*text) {
                     case '\n':
                     case '\r':
                     case '\t':
                     case '"':
                         return true;
                     default:
                         break;
                 }
                 break;
 
             default:    // Invalid enum value
                 CRM_ASSERT(false);
                 break;
         }
 
         text = g_utf8_next_char(text);
     }
     return false;
 }
 
 /*!
  * \internal
  * \brief Replace special characters with their XML escape sequences
  *
  * \param[in] text  Text to escape
  * \param[in] type  Type of escaping
  *
  * \return Newly allocated string equivalent to \p text but with special
  *         characters replaced with XML escape sequences (or \c NULL if \p text
  *         is \c NULL). If \p text is not \c NULL, the return value is
  *         guaranteed not to be \c NULL.
  *
  * \note There are libxml functions that purport to do this:
  *       \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars().
  *       However, their escaping is incomplete. See:
  *       https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252
  * \note The caller is responsible for freeing the return value using
  *       \c g_free().
  */
 gchar *
 pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type)
 {
     GString *copy = NULL;
 
     if (text == NULL) {
         return NULL;
     }
     copy = g_string_sized_new(strlen(text));
 
     while (*text != '\0') {
         // Don't escape any non-ASCII characters
         if ((*text & 0x80) != 0) {
             size_t bytes = g_utf8_next_char(text) - text;
 
             g_string_append_len(copy, text, bytes);
             text += bytes;
             continue;
         }
 
         switch (type) {
             case pcmk__xml_escape_text:
                 switch (*text) {
                     case '<':
                         g_string_append(copy, PCMK__XML_ENTITY_LT);
                         break;
                     case '>':
                         g_string_append(copy, PCMK__XML_ENTITY_GT);
                         break;
                     case '&':
                         g_string_append(copy, PCMK__XML_ENTITY_AMP);
                         break;
                     case '\n':
                     case '\t':
                         g_string_append_c(copy, *text);
                         break;
                     default:
                         if (g_ascii_iscntrl(*text)) {
                             g_string_append_printf(copy, "&#x%.2X;", *text);
                         } else {
                             g_string_append_c(copy, *text);
                         }
                         break;
                 }
                 break;
 
             case pcmk__xml_escape_attr:
                 switch (*text) {
                     case '<':
                         g_string_append(copy, PCMK__XML_ENTITY_LT);
                         break;
                     case '>':
                         g_string_append(copy, PCMK__XML_ENTITY_GT);
                         break;
                     case '&':
                         g_string_append(copy, PCMK__XML_ENTITY_AMP);
                         break;
                     case '"':
                         g_string_append(copy, PCMK__XML_ENTITY_QUOT);
                         break;
                     default:
                         if (g_ascii_iscntrl(*text)) {
                             g_string_append_printf(copy, "&#x%.2X;", *text);
                         } else {
                             g_string_append_c(copy, *text);
                         }
                         break;
                 }
                 break;
 
             case pcmk__xml_escape_attr_pretty:
                 switch (*text) {
                     case '"':
                         g_string_append(copy, "\\\"");
                         break;
                     case '\n':
                         g_string_append(copy, "\\n");
                         break;
                     case '\r':
                         g_string_append(copy, "\\r");
                         break;
                     case '\t':
                         g_string_append(copy, "\\t");
                         break;
                     default:
                         g_string_append_c(copy, *text);
                         break;
                 }
                 break;
 
             default:    // Invalid enum value
                 CRM_ASSERT(false);
                 break;
         }
 
         text = g_utf8_next_char(text);
     }
     return g_string_free(copy, FALSE);
 }
 
 /*!
  * \internal
  * \brief Set a flag on all attributes of an XML element
  *
  * \param[in,out] xml   XML node to set flags on
  * \param[in]     flag  XML private flag to set
  */
 static void
 set_attrs_flag(xmlNode *xml, enum xml_private_flags flag)
 {
     for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) {
         pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag);
     }
 }
 
 /*!
  * \internal
  * \brief Add an XML attribute to a node, marked as deleted
  *
  * When calculating XML changes, we need to know when an attribute has been
  * deleted. Add the attribute back to the new XML, so that we can check the
  * removal against ACLs, and mark it as deleted for later removal after
  * differences have been calculated.
  *
  * \param[in,out] new_xml     XML to modify
  * \param[in]     element     Name of XML element that changed (for logging)
  * \param[in]     attr_name   Name of attribute that was deleted
  * \param[in]     old_value   Value of attribute that was deleted
  */
 static void
 mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name,
                   const char *old_value)
 {
     xml_doc_private_t *docpriv = new_xml->doc->_private;
     xmlAttr *attr = NULL;
     xml_node_private_t *nodepriv;
 
     // Prevent the dirty flag being set recursively upwards
     pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking);
 
     // Restore the old value (and the tracking flag)
     attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value);
     pcmk__set_xml_flags(docpriv, pcmk__xf_tracking);
 
     // Reset flags (so the attribute doesn't appear as newly created)
     nodepriv = attr->_private;
     nodepriv->flags = 0;
 
     // Check ACLs and mark restored value for later removal
     remove_xe_attr(new_xml, attr);
 
     crm_trace("XML attribute %s=%s was removed from %s",
               attr_name, old_value, element);
 }
 
 /*
  * \internal
  * \brief Check ACLs for a changed XML attribute
  */
 static void
 mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name,
                   const char *old_value)
 {
     char *vcopy = crm_element_value_copy(new_xml, attr_name);
 
     crm_trace("XML attribute %s was changed from '%s' to '%s' in %s",
               attr_name, old_value, vcopy, element);
 
     // Restore the original value
     xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value);
 
     // Change it back to the new value, to check ACLs
     crm_xml_add(new_xml, attr_name, vcopy);
     free(vcopy);
 }
 
 /*!
  * \internal
  * \brief Mark an XML attribute as having changed position
  *
  * \param[in,out] new_xml     XML to modify
  * \param[in]     element     Name of XML element that changed (for logging)
  * \param[in,out] old_attr    Attribute that moved, in original XML
  * \param[in,out] new_attr    Attribute that moved, in \p new_xml
  * \param[in]     p_old       Ordinal position of \p old_attr in original XML
  * \param[in]     p_new       Ordinal position of \p new_attr in \p new_xml
  */
 static void
 mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr,
                 xmlAttr *new_attr, int p_old, int p_new)
 {
     xml_node_private_t *nodepriv = new_attr->_private;
 
     crm_trace("XML attribute %s moved from position %d to %d in %s",
               old_attr->name, p_old, p_new, element);
 
     // Mark document, element, and all element's parents as changed
     pcmk__mark_xml_node_dirty(new_xml);
 
     // Mark attribute as changed
     pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved);
 
     nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private;
     pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
 }
 
 /*!
  * \internal
  * \brief Calculate differences in all previously existing XML attributes
  *
  * \param[in,out] old_xml  Original XML to compare
  * \param[in,out] new_xml  New XML to compare
  */
 static void
 xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml)
 {
     xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml);
 
     while (attr_iter != NULL) {
         const char *name = (const char *) attr_iter->name;
         xmlAttr *old_attr = attr_iter;
         xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name);
         const char *old_value = pcmk__xml_attr_value(attr_iter);
 
         attr_iter = attr_iter->next;
         if (new_attr == NULL) {
             mark_attr_deleted(new_xml, (const char *) old_xml->name, name,
                               old_value);
 
         } else {
             xml_node_private_t *nodepriv = new_attr->_private;
             int new_pos = pcmk__xml_position((xmlNode*) new_attr,
                                              pcmk__xf_skip);
             int old_pos = pcmk__xml_position((xmlNode*) old_attr,
                                              pcmk__xf_skip);
             const char *new_value = crm_element_value(new_xml, name);
 
             // This attribute isn't new
             pcmk__clear_xml_flags(nodepriv, pcmk__xf_created);
 
             if (strcmp(new_value, old_value) != 0) {
                 mark_attr_changed(new_xml, (const char *) old_xml->name, name,
                                   old_value);
 
             } else if ((old_pos != new_pos)
                        && !pcmk__tracking_xml_changes(new_xml, TRUE)) {
                 mark_attr_moved(new_xml, (const char *) old_xml->name,
                                 old_attr, new_attr, old_pos, new_pos);
             }
         }
     }
 }
 
 /*!
  * \internal
  * \brief Check all attributes in new XML for creation
  *
  * For each of a given XML element's attributes marked as newly created, accept
  * (and mark as dirty) or reject the creation according to ACLs.
  *
  * \param[in,out] new_xml  XML to check
  */
 static void
 mark_created_attrs(xmlNode *new_xml)
 {
     xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml);
 
     while (attr_iter != NULL) {
         xmlAttr *new_attr = attr_iter;
         xml_node_private_t *nodepriv = attr_iter->_private;
 
         attr_iter = attr_iter->next;
         if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
             const char *attr_name = (const char *) new_attr->name;
 
             crm_trace("Created new attribute %s=%s in %s",
                       attr_name, pcmk__xml_attr_value(new_attr),
                       new_xml->name);
 
             /* Check ACLs (we can't use the remove-then-create trick because it
              * would modify the attribute position).
              */
             if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) {
                 pcmk__mark_xml_attr_dirty(new_attr);
             } else {
                 // Creation was not allowed, so remove the attribute
                 xmlUnsetProp(new_xml, new_attr->name);
             }
         }
     }
 }
 
 /*!
  * \internal
  * \brief Calculate differences in attributes between two XML nodes
  *
  * \param[in,out] old_xml  Original XML to compare
  * \param[in,out] new_xml  New XML to compare
  */
 static void
 xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml)
 {
     set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new
     xml_diff_old_attrs(old_xml, new_xml);
     mark_created_attrs(new_xml);
 }
 
 /*!
  * \internal
  * \brief Add an XML child element to a node, marked as deleted
  *
  * When calculating XML changes, we need to know when a child element has been
  * deleted. Add the child back to the new XML, so that we can check the removal
  * against ACLs, and mark it as deleted for later removal after differences have
  * been calculated.
  *
  * \param[in,out] old_child    Child element from original XML
  * \param[in,out] new_parent   New XML to add marked copy to
  */
 static void
 mark_child_deleted(xmlNode *old_child, xmlNode *new_parent)
 {
     // Re-create the child element so we can check ACLs
     xmlNode *candidate = pcmk__xml_copy(new_parent, old_child);
 
     // Clear flags on new child and its children
     pcmk__xml_tree_foreach(candidate, reset_xml_node_flags, NULL);
 
     // Check whether ACLs allow the deletion
     pcmk__apply_acl(xmlDocGetRootElement(candidate->doc));
 
     // Remove the child again (which will track it in document's deleted_objs)
     free_xml_with_position(candidate,
                            pcmk__xml_position(old_child, pcmk__xf_skip));
 
     if (pcmk__xml_match(new_parent, old_child, true) == NULL) {
         pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private),
                             pcmk__xf_skip);
     }
 }
 
 static void
 mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child,
                  int p_old, int p_new)
 {
     xml_node_private_t *nodepriv = new_child->_private;
 
     crm_trace("Child element %s with "
               PCMK_XA_ID "='%s' moved from position %d to %d under %s",
               new_child->name, pcmk__s(pcmk__xe_id(new_child), "<no id>"),
               p_old, p_new, new_parent->name);
     pcmk__mark_xml_node_dirty(new_parent);
     pcmk__set_xml_flags(nodepriv, pcmk__xf_moved);
 
     if (p_old > p_new) {
         nodepriv = old_child->_private;
     } else {
         nodepriv = new_child->_private;
     }
     pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
 }
 
 // Given original and new XML, mark new XML portions that have changed
 static void
 mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top)
 {
     xmlNode *old_child = NULL;
     xmlNode *new_child = NULL;
     xml_node_private_t *nodepriv = NULL;
 
     CRM_CHECK(new_xml != NULL, return);
     if (old_xml == NULL) {
         pcmk__xml_mark_created(new_xml);
         pcmk__apply_creation_acl(new_xml, check_top);
         return;
     }
 
     nodepriv = new_xml->_private;
     CRM_CHECK(nodepriv != NULL, return);
 
     if(nodepriv->flags & pcmk__xf_processed) {
         /* Avoid re-comparing nodes */
         return;
     }
     pcmk__set_xml_flags(nodepriv, pcmk__xf_processed);
 
     xml_diff_attrs(old_xml, new_xml);
 
     // Check for differences in the original children
     for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL;
          old_child = pcmk__xml_next(old_child)) {
 
         new_child = pcmk__xml_match(new_xml, old_child, true);
 
         if (new_child != NULL) {
             mark_xml_changes(old_child, new_child, true);
 
         } else {
             mark_child_deleted(old_child, new_xml);
         }
     }
 
     // Check for moved or created children
     new_child = pcmk__xml_first_child(new_xml);
     while (new_child != NULL) {
         xmlNode *next = pcmk__xml_next(new_child);
 
         old_child = pcmk__xml_match(old_xml, new_child, true);
 
         if (old_child == NULL) {
             // This is a newly created child
             nodepriv = new_child->_private;
             pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
 
             // May free new_child
             mark_xml_changes(old_child, new_child, true);
 
         } else {
             /* Check for movement, we already checked for differences */
             int p_new = pcmk__xml_position(new_child, pcmk__xf_skip);
             int p_old = pcmk__xml_position(old_child, pcmk__xf_skip);
 
             if(p_old != p_new) {
                 mark_child_moved(old_child, new_xml, new_child, p_old, p_new);
             }
         }
 
         new_child = next;
     }
 }
 
 void
 xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml)
 {
     pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy);
     xml_calculate_changes(old_xml, new_xml);
 }
 
 // Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml
 void
 xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml)
 {
     CRM_CHECK((old_xml != NULL) && (new_xml != NULL)
               && pcmk__xe_is(old_xml, (const char *) new_xml->name)
               && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml),
                               pcmk__str_none),
               return);
 
     if(xml_tracking_changes(new_xml) == FALSE) {
         xml_track_changes(new_xml, NULL, NULL, FALSE);
     }
 
     mark_xml_changes(old_xml, new_xml, FALSE);
 }
 
 /*!
  * \internal
  * \brief Find a comment with matching content in specified XML
  *
  * \param[in] root            XML to search
  * \param[in] search_comment  Comment whose content should be searched for
  * \param[in] exact           If true, comment must also be at same position
  */
 xmlNode *
 pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact)
 {
     xmlNode *a_child = NULL;
     int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip);
 
     CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL);
 
     for (a_child = pcmk__xml_first_child(root); a_child != NULL;
          a_child = pcmk__xml_next(a_child)) {
         if (exact) {
             int offset = pcmk__xml_position(a_child, pcmk__xf_skip);
             xml_node_private_t *nodepriv = a_child->_private;
 
             if (offset < search_offset) {
                 continue;
 
             } else if (offset > search_offset) {
                 return NULL;
             }
 
             if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) {
                 continue;
             }
         }
 
         if (a_child->type == XML_COMMENT_NODE
             && pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) {
             return a_child;
 
         } else if (exact) {
             return NULL;
         }
     }
 
     return NULL;
 }
 
 /*!
  * \internal
  * \brief Make one XML comment match another (in content)
  *
  * \param[in,out] parent   If \p target is NULL and this is not, add or update
  *                         comment child of this XML node that matches \p update
  * \param[in,out] target   If not NULL, update this XML comment node
  * \param[in]     update   Make comment content match this (must not be NULL)
  *
  * \note At least one of \parent and \target must be non-NULL
  */
 void
 pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update)
 {
     CRM_CHECK(update != NULL, return);
     CRM_CHECK(update->type == XML_COMMENT_NODE, return);
 
     if (target == NULL) {
         target = pcmk__xc_match(parent, update, false);
     }
 
     if (target == NULL) {
         pcmk__xml_copy(parent, update);
 
     } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) {
         xmlFree(target->content);
         target->content = xmlStrdup(update->content);
     }
 }
 
 /*!
  * \internal
  * \brief Merge one XML tree into another
  *
  * Here, "merge" means:
  * 1. Copy attribute values from \p update to the target, overwriting in case of
  *    conflict.
  * 2. Descend through \p update and the target in parallel. At each level, for
  *    each child of \p update, look for a matching child of the target.
  *    a. For each child, if a match is found, go to step 1, recursively merging
  *       the child of \p update into the child of the target.
  *    b. Otherwise, copy the child of \p update as a child of the target.
  *
  * A match is defined as the first child of the same type within the target,
  * with:
  * * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise,
  * * the \c PCMK_XA_ID_REF attribute matching, if set in \p update
  *
  * This function does not delete any elements or attributes from the target. It
  * may add elements or overwrite attributes, as described above.
  *
  * \param[in,out] parent   If \p target is NULL and this is not, add or update
  *                         child of this XML node that matches \p update
  * \param[in,out] target   If not NULL, update this XML
  * \param[in]     update   Make the desired XML match this (must not be \c NULL)
  * \param[in]     flags    Group of <tt>enum pcmk__xa_flags</tt>
  * \param[in]     as_diff  If \c true, preserve order of attributes (deprecated
  *                         since 2.0.5)
  *
  * \note At least one of \p parent and \p target must be non-<tt>NULL</tt>.
  * \note This function is recursive. For the top-level call, \p parent is
  *       \c NULL and \p target is not \c NULL. For recursive calls, \p target is
  *       \c NULL and \p parent is not \c NULL.
  */
 void
 pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update,
                  uint32_t flags, bool as_diff)
 {
     /* @COMPAT Refactor further and staticize after v1 patchset deprecation.
      *
      * @COMPAT Drop as_diff argument when apply_xml_diff() is dropped.
      */
     const char *update_name = NULL;
     const char *update_id_attr = NULL;
     const char *update_id_val = NULL;
     char *trace_s = NULL;
 
     crm_log_xml_trace(update, "update");
     crm_log_xml_trace(target, "target");
 
     CRM_CHECK(update != NULL, goto done);
 
     if (update->type == XML_COMMENT_NODE) {
         pcmk__xc_update(parent, target, update);
         goto done;
     }
 
     update_name = (const char *) update->name;
 
     CRM_CHECK(update_name != NULL, goto done);
     CRM_CHECK((target != NULL) || (parent != NULL), goto done);
 
     update_id_val = pcmk__xe_id(update);
     if (update_id_val != NULL) {
         update_id_attr = PCMK_XA_ID;
 
     } else {
         update_id_val = crm_element_value(update, PCMK_XA_ID_REF);
         if (update_id_val != NULL) {
             update_id_attr = PCMK_XA_ID_REF;
         }
     }
 
     pcmk__if_tracing(
         {
             if (update_id_attr != NULL) {
                 trace_s = crm_strdup_printf("<%s %s=%s/>",
                                             update_name, update_id_attr,
                                             update_id_val);
             } else {
                 trace_s = crm_strdup_printf("<%s/>", update_name);
             }
         },
         {}
     );
 
     if (target == NULL) {
         // Recursive call
         target = pcmk__xe_first_child(parent, update_name, update_id_attr,
                                       update_id_val);
     }
 
     if (target == NULL) {
         // Recursive call with no existing matching child
         target = pcmk__xe_create(parent, update_name);
         crm_trace("Added %s", pcmk__s(trace_s, update_name));
 
     } else {
         // Either recursive call with match, or top-level call
         crm_trace("Found node %s to update", pcmk__s(trace_s, update_name));
     }
 
     CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return);
 
     if (!as_diff) {
         pcmk__xe_copy_attrs(target, update, flags);
 
     } else {
         // Preserve order of attributes. Don't use pcmk__xe_copy_attrs().
         for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL;
              a = a->next) {
             const char *p_value = pcmk__xml_attr_value(a);
 
             /* Remove it first so the ordering of the update is preserved */
             xmlUnsetProp(target, a->name);
             xmlSetProp(target, a->name, (pcmkXmlStr) p_value);
         }
     }
 
     for (xmlNode *child = pcmk__xml_first_child(update); child != NULL;
          child = pcmk__xml_next(child)) {
 
         crm_trace("Updating child of %s", pcmk__s(trace_s, update_name));
         pcmk__xml_update(target, NULL, child, flags, as_diff);
     }
 
     crm_trace("Finished with %s", pcmk__s(trace_s, update_name));
 
 done:
     free(trace_s);
 }
 
 /*!
  * \internal
  * \brief Delete an XML subtree if it matches a search element
  *
  * A match is defined as follows:
  * * \p xml and \p user_data are both element nodes of the same type.
  * * If \p user_data has attributes set, \p xml has those attributes set to the
  *   same values. (\p xml may have additional attributes set to arbitrary
  *   values.)
  *
  * \param[in,out] xml        XML subtree to delete upon match
  * \param[in]     user_data  Search element
  *
  * \return \c true to continue traversing the tree, or \c false to stop (because
  *         \p xml was deleted)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 delete_xe_if_matching(xmlNode *xml, void *user_data)
 {
     xmlNode *search = user_data;
 
     if (!pcmk__xe_is(search, (const char *) xml->name)) {
         // No match: either not both elements, or different element types
         return true;
     }
 
     for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL;
          attr = attr->next) {
 
         const char *search_val = pcmk__xml_attr_value(attr);
         const char *xml_val = crm_element_value(xml, (const char *) attr->name);
 
         if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) {
             // No match: an attr in xml doesn't match the attr in search
             return true;
         }
     }
 
     crm_log_xml_trace(xml, "delete-match");
     crm_log_xml_trace(search, "delete-search");
     free_xml(xml);
 
     // Found a match and deleted it; stop traversing tree
     return false;
 }
 
 /*!
  * \internal
  * \brief Search an XML tree depth-first and delete the first matching element
  *
  * This function does not attempt to match the tree root (\p xml).
  *
  * A match with a node \c node is defined as follows:
  * * \c node and \p search are both element nodes of the same type.
  * * If \p search has attributes set, \c node has those attributes set to the
  *   same values. (\c node may have additional attributes set to arbitrary
  *   values.)
  *
  * \param[in,out] xml     XML subtree to search
  * \param[in]     search  Element to match against
  *
  * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
  *         successful deletion and an error code otherwise)
  */
 int
 pcmk__xe_delete_match(xmlNode *xml, xmlNode *search)
 {
     // See @COMPAT comment in pcmk__xe_replace_match()
     CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL);
 
     for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
          xml = pcmk__xe_next(xml)) {
 
         if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) {
             // Found and deleted an element
             return pcmk_rc_ok;
         }
     }
 
     // No match found in this subtree
     return ENXIO;
 }
 
 /*!
  * \internal
  * \brief Replace one XML node with a copy of another XML node
  *
  * This function handles change tracking and applies ACLs.
  *
  * \param[in,out] old  XML node to replace
  * \param[in]     new  XML node to copy as replacement for \p old
  *
  * \note This frees \p old.
  */
 static void
 replace_node(xmlNode *old, xmlNode *new)
 {
     new = xmlCopyNode(new, 1);
     pcmk__mem_assert(new);
 
     // May be unnecessary but avoids slight changes to some test outputs
     pcmk__xml_tree_foreach(new, reset_xml_node_flags, NULL);
 
     old = xmlReplaceNode(old, new);
 
     if (xml_tracking_changes(new)) {
         // Replaced sections may have included relevant ACLs
         pcmk__apply_acl(new);
     }
     xml_calculate_changes(old, new);
-    xmlFreeNode(old);
+    pcmk_free_xml_subtree(old);
 }
 
 /*!
  * \internal
  * \brief Replace one XML subtree with a copy of another if the two match
  *
  * A match is defined as follows:
  * * \p xml and \p user_data are both element nodes of the same type.
  * * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has
  *   \c PCMK_XA_ID set to the same value.
  *
  * \param[in,out] xml        XML subtree to replace with \p user_data upon match
  * \param[in]     user_data  XML to replace \p xml with a copy of upon match
  *
  * \return \c true to continue traversing the tree, or \c false to stop (because
  *         \p xml was replaced by \p user_data)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 replace_xe_if_matching(xmlNode *xml, void *user_data)
 {
     xmlNode *replace = user_data;
     const char *xml_id = NULL;
     const char *replace_id = NULL;
 
     xml_id = pcmk__xe_id(xml);
     replace_id = pcmk__xe_id(replace);
 
     if (!pcmk__xe_is(replace, (const char *) xml->name)) {
         // No match: either not both elements, or different element types
         return true;
     }
 
     if ((replace_id != NULL)
         && !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) {
 
         // No match: ID was provided in replace and doesn't match xml's ID
         return true;
     }
 
     crm_log_xml_trace(xml, "replace-match");
     crm_log_xml_trace(replace, "replace-with");
     replace_node(xml, replace);
 
     // Found a match and replaced it; stop traversing tree
     return false;
 }
 
 /*!
  * \internal
  * \brief Search an XML tree depth-first and replace the first matching element
  *
  * This function does not attempt to match the tree root (\p xml).
  *
  * A match with a node \c node is defined as follows:
  * * \c node and \p replace are both element nodes of the same type.
  * * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has
  *   \c PCMK_XA_ID set to the same value.
  *
  * \param[in,out] xml      XML tree to search
  * \param[in]     replace  XML to replace a matching element with a copy of
  *
  * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
  *         successful replacement and an error code otherwise)
  */
 int
 pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace)
 {
     /* @COMPAT Some of this behavior (like not matching the tree root, which is
      * allowed by pcmk__xe_update_match()) is questionable for general use but
      * required for backward compatibility by cib_process_replace() and
      * cib_process_delete(). Behavior can change at a major version release if
      * desired.
      */
     CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL);
 
     for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
          xml = pcmk__xe_next(xml)) {
 
         if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) {
             // Found and replaced an element
             return pcmk_rc_ok;
         }
     }
 
     // No match found in this subtree
     return ENXIO;
 }
 
 //! User data for \c update_xe_if_matching()
 struct update_data {
     xmlNode *update;    //!< Update source
     uint32_t flags;     //!< Group of <tt>enum pcmk__xa_flags</tt>
 };
 
 /*!
  * \internal
  * \brief Update one XML subtree with another if the two match
  *
  * "Update" means to merge a source subtree into a target subtree (see
  * \c pcmk__xml_update()).
  *
  * A match is defined as follows:
  * * \p xml and \p user_data->update are both element nodes of the same type.
  * * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute
  *   value, or \c PCMK_XA_ID is unset in both
  *
  * \param[in,out] xml        XML subtree to update with \p user_data->update
  *                           upon match
  * \param[in]     user_data  <tt>struct update_data</tt> object
  *
  * \return \c true to continue traversing the tree, or \c false to stop (because
  *         \p xml was updated by \p user_data->update)
  *
  * \note This is compatible with \c pcmk__xml_tree_foreach().
  */
 static bool
 update_xe_if_matching(xmlNode *xml, void *user_data)
 {
     struct update_data *data = user_data;
     xmlNode *update = data->update;
 
     if (!pcmk__xe_is(update, (const char *) xml->name)) {
         // No match: either not both elements, or different element types
         return true;
     }
 
     if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) {
         // No match: ID mismatch
         return true;
     }
 
     crm_log_xml_trace(xml, "update-match");
     crm_log_xml_trace(update, "update-with");
     pcmk__xml_update(NULL, xml, update, data->flags, false);
 
     // Found a match and replaced it; stop traversing tree
     return false;
 }
 
 /*!
  * \internal
  * \brief Search an XML tree depth-first and update the first matching element
  *
  * "Update" means to merge a source subtree into a target subtree (see
  * \c pcmk__xml_update()).
  *
  * A match with a node \c node is defined as follows:
  * * \c node and \p update are both element nodes of the same type.
  * * \c node and \p update have the same \c PCMK_XA_ID attribute value, or
  *   \c PCMK_XA_ID is unset in both
  *
  * \param[in,out] xml     XML tree to search
  * \param[in]     update  XML to update a matching element with
  * \param[in]     flags   Group of <tt>enum pcmk__xa_flags</tt>
  *
  * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
  *         successful update and an error code otherwise)
  */
 int
 pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags)
 {
     /* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we
      * compare IDs only if the equivalent of the update argument has an ID.
      * Here, we're stricter: we consider it a mismatch if only one element has
      * an ID attribute, or if both elements have IDs but they don't match.
      *
      * Perhaps we should align the behavior at a major version release.
      */
     struct update_data data = {
         .update = update,
         .flags = flags,
     };
 
     CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL);
 
     if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) {
         // Found and updated an element
         return pcmk_rc_ok;
     }
 
     // No match found in this subtree
     return ENXIO;
 }
 
 xmlNode *
 sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive)
 {
     xmlNode *child = NULL;
     GSList *nvpairs = NULL;
     xmlNode *result = NULL;
 
     CRM_CHECK(input != NULL, return NULL);
 
     result = pcmk__xe_create(parent, (const char *) input->name);
     nvpairs = pcmk_xml_attrs2nvpairs(input);
     nvpairs = pcmk_sort_nvpairs(nvpairs);
     pcmk_nvpairs2xml_attrs(nvpairs, result);
     pcmk_free_nvpairs(nvpairs);
 
     for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL;
          child = pcmk__xe_next(child)) {
 
         if (recursive) {
             sorted_xml(child, result, recursive);
         } else {
             pcmk__xml_copy(result, child);
         }
     }
 
     return result;
 }
 
 /*!
  * \internal
  * \brief Get next sibling XML element with the same name as a given element
  *
  * \param[in] node  XML element to start from
  *
  * \return Next sibling XML element with same name
  */
 xmlNode *
 pcmk__xe_next_same(const xmlNode *node)
 {
     for (xmlNode *match = pcmk__xe_next(node); match != NULL;
          match = pcmk__xe_next(match)) {
 
         if (pcmk__xe_is(match, (const char *) node->name)) {
             return match;
         }
     }
     return NULL;
 }
 
 void
 crm_xml_init(void)
 {
     static bool init = true;
 
     if(init) {
         init = false;
         /* The default allocator XML_BUFFER_ALLOC_EXACT does far too many
          * pcmk__realloc()s and it can take upwards of 18 seconds (yes, seconds)
          * to dump a 28kb tree which XML_BUFFER_ALLOC_DOUBLEIT can do in
          * less than 1 second.
          */
         xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT);
 
         /* Populate and free the _private field when nodes are created and destroyed */
         xmlDeregisterNodeDefault(free_private_data);
         xmlRegisterNodeDefault(new_private_data);
 
         crm_schema_init();
     }
 }
 
 void
 crm_xml_cleanup(void)
 {
     crm_schema_cleanup();
     xmlCleanupParser();
 }
 
 #define XPATH_MAX 512
 
 xmlNode *
 expand_idref(xmlNode * input, xmlNode * top)
 {
     char *xpath = NULL;
     const char *ref = NULL;
     xmlNode *result = NULL;
 
     if (input == NULL) {
         return NULL;
     }
 
     ref = crm_element_value(input, PCMK_XA_ID_REF);
     if (ref == NULL) {
         return input;
     }
 
     if (top == NULL) {
         top = input;
     }
 
     xpath = crm_strdup_printf("//%s[@" PCMK_XA_ID "='%s']", input->name, ref);
     result = get_xpath_object(xpath, top, LOG_DEBUG);
     if (result == NULL) { // Not possible with schema validation enabled
         pcmk__config_err("Ignoring invalid %s configuration: "
                          PCMK_XA_ID_REF " '%s' does not reference "
                          "a valid object " CRM_XS " xpath=%s",
                          input->name, ref, xpath);
     }
     free(xpath);
     return result;
 }
 
 char *
 pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns)
 {
     static const char *base = NULL;
     char *ret = NULL;
 
     if (base == NULL) {
         base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY);
     }
     if (pcmk__str_empty(base)) {
         base = CRM_SCHEMA_DIRECTORY;
     }
 
     switch (ns) {
         case pcmk__xml_artefact_ns_legacy_rng:
         case pcmk__xml_artefact_ns_legacy_xslt:
             ret = strdup(base);
             break;
         case pcmk__xml_artefact_ns_base_rng:
         case pcmk__xml_artefact_ns_base_xslt:
             ret = crm_strdup_printf("%s/base", base);
             break;
         default:
             crm_err("XML artefact family specified as %u not recognized", ns);
     }
     return ret;
 }
 
 static char *
 find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec)
 {
     char *ret = NULL;
 
     switch (ns) {
         case pcmk__xml_artefact_ns_legacy_rng:
         case pcmk__xml_artefact_ns_base_rng:
             if (pcmk__ends_with(filespec, ".rng")) {
                 ret = crm_strdup_printf("%s/%s", path, filespec);
             } else {
                 ret = crm_strdup_printf("%s/%s.rng", path, filespec);
             }
             break;
         case pcmk__xml_artefact_ns_legacy_xslt:
         case pcmk__xml_artefact_ns_base_xslt:
             if (pcmk__ends_with(filespec, ".xsl")) {
                 ret = crm_strdup_printf("%s/%s", path, filespec);
             } else {
                 ret = crm_strdup_printf("%s/%s.xsl", path, filespec);
             }
             break;
         default:
             crm_err("XML artefact family specified as %u not recognized", ns);
     }
 
     return ret;
 }
 
 char *
 pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec)
 {
     struct stat sb;
     char *base = pcmk__xml_artefact_root(ns);
     char *ret = NULL;
 
     ret = find_artefact(ns, base, filespec);
     free(base);
 
     if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) {
         const char *remote_schema_dir = pcmk__remote_schema_dir();
 
         free(ret);
         ret = find_artefact(ns, remote_schema_dir, filespec);
     }
 
     return ret;
 }
 
 void
 pcmk__xe_set_propv(xmlNodePtr node, va_list pairs)
 {
     while (true) {
         const char *name, *value;
 
         name = va_arg(pairs, const char *);
         if (name == NULL) {
             return;
         }
 
         value = va_arg(pairs, const char *);
         if (value != NULL) {
             crm_xml_add(node, name, value);
         }
     }
 }
 
 void
 pcmk__xe_set_props(xmlNodePtr node, ...)
 {
     va_list pairs;
     va_start(pairs, node);
     pcmk__xe_set_propv(node, pairs);
     va_end(pairs);
 }
 
 int
 pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name,
                        int (*handler)(xmlNode *xml, void *userdata),
                        void *userdata)
 {
     xmlNode *children = (xml? xml->children : NULL);
 
     CRM_ASSERT(handler != NULL);
 
     for (xmlNode *node = children; node != NULL; node = node->next) {
         if ((node->type == XML_ELEMENT_NODE)
             && ((child_element_name == NULL)
                 || pcmk__xe_is(node, child_element_name))) {
             int rc = handler(node, userdata);
 
             if (rc != pcmk_rc_ok) {
                 return rc;
             }
         }
     }
 
     return pcmk_rc_ok;
 }
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/common/xml_compat.h>
 
 xmlNode *
 find_entity(xmlNode *parent, const char *node_name, const char *id)
 {
     return pcmk__xe_first_child(parent, node_name,
                                 ((id == NULL)? id : PCMK_XA_ID), id);
 }
 
 void
 crm_destroy_xml(gpointer data)
 {
     free_xml(data);
 }
 
 xmlDoc *
 getDocPtr(xmlNode *node)
 {
     xmlDoc *doc = NULL;
 
     CRM_CHECK(node != NULL, return NULL);
 
     doc = node->doc;
     if (doc == NULL) {
         doc = xmlNewDoc(PCMK__XML_VERSION);
         xmlDocSetRootElement(doc, node);
     }
     return doc;
 }
 
 xmlNode *
 add_node_copy(xmlNode *parent, xmlNode *src_node)
 {
     xmlNode *child = NULL;
 
     CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL);
 
     child = xmlDocCopyNode(src_node, parent->doc, 1);
     if (child == NULL) {
         return NULL;
     }
     xmlAddChild(parent, child);
     pcmk__xml_mark_created(child);
     return child;
 }
 
 int
 add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child)
 {
     add_node_copy(parent, child);
     free_xml(child);
     return 1;
 }
 
 gboolean
 xml_has_children(const xmlNode * xml_root)
 {
     if (xml_root != NULL && xml_root->children != NULL) {
         return TRUE;
     }
     return FALSE;
 }
 
 static char *
 replace_text(char *text, size_t *index, size_t *length, const char *replace)
 {
     // We have space for 1 char already
     size_t offset = strlen(replace) - 1;
 
     if (offset > 0) {
         *length += offset;
         text = pcmk__realloc(text, *length + 1);
 
         // Shift characters to the right to make room for the replacement string
         for (size_t i = *length; i > (*index + offset); i--) {
             text[i] = text[i - offset];
         }
     }
 
     // Replace the character at index by the replacement string
     memcpy(text + *index, replace, offset + 1);
 
     // Reset index to the end of replacement string
     *index += offset;
     return text;
 }
 
 char *
 crm_xml_escape(const char *text)
 {
     size_t length = 0;
     char *copy = NULL;
 
     if (text == NULL) {
         return NULL;
     }
 
     length = strlen(text);
     copy = pcmk__str_copy(text);
     for (size_t index = 0; index <= length; index++) {
         if(copy[index] & 0x80 && copy[index+1] & 0x80){
             index++;
             continue;
         }
         switch (copy[index]) {
             case 0:
                 // Sanity only; loop should stop at the last non-null byte
                 break;
             case '<':
                 copy = replace_text(copy, &index, &length, "&lt;");
                 break;
             case '>':
                 copy = replace_text(copy, &index, &length, "&gt;");
                 break;
             case '"':
                 copy = replace_text(copy, &index, &length, "&quot;");
                 break;
             case '\'':
                 copy = replace_text(copy, &index, &length, "&apos;");
                 break;
             case '&':
                 copy = replace_text(copy, &index, &length, "&amp;");
                 break;
             case '\t':
                 /* Might as well just expand to a few spaces... */
                 copy = replace_text(copy, &index, &length, "    ");
                 break;
             case '\n':
                 copy = replace_text(copy, &index, &length, "\\n");
                 break;
             case '\r':
                 copy = replace_text(copy, &index, &length, "\\r");
                 break;
             default:
                 /* Check for and replace non-printing characters with their octal equivalent */
                 if(copy[index] < ' ' || copy[index] > '~') {
                     char *replace = crm_strdup_printf("\\%.3o", copy[index]);
 
                     copy = replace_text(copy, &index, &length, replace);
                     free(replace);
                 }
         }
     }
     return copy;
 }
 
 xmlNode *
 copy_xml(xmlNode *src)
 {
     xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION);
     xmlNode *copy = NULL;
 
     pcmk__mem_assert(doc);
 
     copy = xmlDocCopyNode(src, doc, 1);
     pcmk__mem_assert(copy);
 
     xmlDocSetRootElement(doc, copy);
     return copy;
 }
 
 xmlNode *
 create_xml_node(xmlNode *parent, const char *name)
 {
     // Like pcmk__xe_create(), but returns NULL on failure
     xmlNode *node = NULL;
 
     CRM_CHECK(!pcmk__str_empty(name), return NULL);
 
     if (parent == NULL) {
         xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION);
 
         if (doc == NULL) {
             return NULL;
         }
 
         node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL);
         if (node == NULL) {
             xmlFreeDoc(doc);
             return NULL;
         }
         xmlDocSetRootElement(doc, node);
 
     } else {
         node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL);
         if (node == NULL) {
             return NULL;
         }
     }
     pcmk__xml_mark_created(node);
     return node;
 }
 
 xmlNode *
 pcmk_create_xml_text_node(xmlNode *parent, const char *name,
                           const char *content)
 {
     xmlNode *node = pcmk__xe_create(parent, name);
 
     pcmk__xe_set_content(node, "%s", content);
     return node;
 }
 
 xmlNode *
 pcmk_create_html_node(xmlNode *parent, const char *element_name, const char *id,
                       const char *class_name, const char *text)
 {
     xmlNode *node = pcmk__html_create(parent, element_name, id, class_name);
 
     pcmk__xe_set_content(node, "%s", text);
     return node;
 }
 
 xmlNode *
 first_named_child(const xmlNode *parent, const char *name)
 {
     return pcmk__xe_first_child(parent, name, NULL, NULL);
 }
 
 xmlNode *
 find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find)
 {
     xmlNode *result = NULL;
 
     if (search_path == NULL) {
         crm_warn("Will never find <NULL>");
         return NULL;
     }
 
     result = pcmk__xe_first_child(root, search_path, NULL, NULL);
 
     if (must_find && (result == NULL)) {
         crm_warn("Could not find %s in %s",
                  search_path,
                  ((root != NULL)? (const char *) root->name : "<NULL>"));
     }
 
     return result;
 }
 
 xmlNode *
 crm_next_same_xml(const xmlNode *sibling)
 {
     return pcmk__xe_next_same(sibling);
 }
 
 void
 xml_remove_prop(xmlNode * obj, const char *name)
 {
     pcmk__xe_remove_attr(obj, name);
 }
 
 gboolean
 replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only)
 {
     bool is_match = false;
     const char *child_id = NULL;
     const char *update_id = NULL;
 
     CRM_CHECK(child != NULL, return FALSE);
     CRM_CHECK(update != NULL, return FALSE);
 
     child_id = pcmk__xe_id(child);
     update_id = pcmk__xe_id(update);
 
     /* Match element name and (if provided in update XML) element ID. Don't
      * match search root (child is search root if parent == NULL).
      */
     is_match = (parent != NULL)
                && pcmk__xe_is(update, (const char *) child->name)
                && ((update_id == NULL)
                    || pcmk__str_eq(update_id, child_id, pcmk__str_none));
 
     /* For deletion, match all attributes provided in update. A matching node
      * can have additional attributes, but values must match for provided ones.
      */
     if (is_match && delete_only) {
         for (xmlAttr *attr = pcmk__xe_first_attr(update); attr != NULL;
              attr = attr->next) {
             const char *name = (const char *) attr->name;
             const char *update_val = pcmk__xml_attr_value(attr);
             const char *child_val = crm_element_value(child, name);
 
             if (!pcmk__str_eq(update_val, child_val, pcmk__str_casei)) {
                 is_match = false;
                 break;
             }
         }
     }
 
     if (is_match) {
         if (delete_only) {
             crm_log_xml_trace(child, "delete-match");
             crm_log_xml_trace(update, "delete-search");
             free_xml(child);
 
         } else {
             crm_log_xml_trace(child, "replace-match");
             crm_log_xml_trace(update, "replace-with");
             replace_node(child, update);
         }
         return TRUE;
     }
 
     // Current node not a match; search the rest of the subtree depth-first
     parent = child;
     for (child = pcmk__xml_first_child(parent); child != NULL;
          child = pcmk__xml_next(child)) {
 
         // Only delete/replace the first match
         if (replace_xml_child(parent, child, update, delete_only)) {
             return TRUE;
         }
     }
 
     // No match found in this subtree
     return FALSE;
 }
 
 gboolean
 update_xml_child(xmlNode *child, xmlNode *to_update)
 {
     return pcmk__xe_update_match(child, to_update,
                                  pcmk__xaf_score_update) == pcmk_rc_ok;
 }
 
 int
 find_xml_children(xmlNode **children, xmlNode *root, const char *tag,
                   const char *field, const char *value, gboolean search_matches)
 {
     int match_found = 0;
 
     CRM_CHECK(root != NULL, return FALSE);
     CRM_CHECK(children != NULL, return FALSE);
 
     if ((tag != NULL) && !pcmk__xe_is(root, tag)) {
 
     } else if ((value != NULL)
                && !pcmk__str_eq(value, crm_element_value(root, field),
                                 pcmk__str_casei)) {
 
     } else {
         if (*children == NULL) {
             *children = pcmk__xe_create(NULL, __func__);
         }
         pcmk__xml_copy(*children, root);
         match_found = 1;
     }
 
     if (search_matches || match_found == 0) {
         xmlNode *child = NULL;
 
         for (child = pcmk__xml_first_child(root); child != NULL;
              child = pcmk__xml_next(child)) {
             match_found += find_xml_children(children, child, tag, field, value,
                                              search_matches);
         }
     }
 
     return match_found;
 }
 
 void
 fix_plus_plus_recursive(xmlNode *target)
 {
     /* TODO: Remove recursion and use xpath searches for value++ */
     xmlNode *child = NULL;
 
     for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) {
         const char *p_name = (const char *) a->name;
         const char *p_value = pcmk__xml_attr_value(a);
 
         expand_plus_plus(target, p_name, p_value);
     }
     for (child = pcmk__xe_first_child(target, NULL, NULL, NULL); child != NULL;
          child = pcmk__xe_next(child)) {
 
         fix_plus_plus_recursive(child);
     }
 }
 
 void
 copy_in_properties(xmlNode *target, const xmlNode *src)
 {
     if (src == NULL) {
         crm_warn("No node to copy properties from");
 
     } else if (target == NULL) {
         crm_err("No node to copy properties into");
 
     } else {
         for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) {
             const char *p_name = (const char *) a->name;
             const char *p_value = pcmk__xml_attr_value(a);
 
             expand_plus_plus(target, p_name, p_value);
             if (xml_acl_denied(target)) {
                 crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name);
                 return;
             }
         }
     }
 }
 
 void
 expand_plus_plus(xmlNode * target, const char *name, const char *value)
 {
     pcmk__xe_set_score(target, name, value);
 }
 
 // LCOV_EXCL_STOP
 // End deprecated API
diff --git a/lib/pacemaker/pcmk_acl.c b/lib/pacemaker/pcmk_acl.c
index 206c71e123..064691cd1b 100644
--- a/lib/pacemaker/pcmk_acl.c
+++ b/lib/pacemaker/pcmk_acl.c
@@ -1,398 +1,398 @@
 /*
  * 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 Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdio.h>
 #include <sys/types.h>
 #include <pwd.h>
 #include <string.h>
 #include <stdlib.h>
 #include <stdarg.h>
 
 #include <libxml/parser.h>
 #include <libxml/tree.h>
 #include <libxml/xpath.h>
 #include <libxslt/transform.h>
 #include <libxslt/variables.h>
 #include <libxslt/xsltutils.h>
 
 #include <crm/crm.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include <crm/common/internal.h>
 
 #include <pacemaker-internal.h>
 
 #define ACL_NS_PREFIX "http://clusterlabs.org/ns/pacemaker/access/"
 #define ACL_NS_Q_PREFIX  "pcmk-access-"
 #define ACL_NS_Q_WRITABLE (const xmlChar *) ACL_NS_Q_PREFIX   "writable"
 #define ACL_NS_Q_READABLE (const xmlChar *) ACL_NS_Q_PREFIX   "readable"
 #define ACL_NS_Q_DENIED   (const xmlChar *) ACL_NS_Q_PREFIX   "denied"
 
 static const xmlChar *NS_WRITABLE = (const xmlChar *) ACL_NS_PREFIX "writable";
 static const xmlChar *NS_READABLE = (const xmlChar *) ACL_NS_PREFIX "readable";
 static const xmlChar *NS_DENIED =   (const xmlChar *) ACL_NS_PREFIX "denied";
 
 /*!
  * \brief This function takes a node and marks it with the namespace
  *        given in the ns parameter.
  *
  * \param[in,out] i_node
  * \param[in] ns
  * \param[in,out] ret
  * \param[in,out] ns_recycle_writable
  * \param[in,out] ns_recycle_readable
  * \param[in,out] ns_recycle_denied
  */
 static void
 pcmk__acl_mark_node_with_namespace(xmlNode *i_node, const xmlChar *ns, int *ret,
                                    xmlNs **ns_recycle_writable,
                                    xmlNs **ns_recycle_readable,
                                    xmlNs **ns_recycle_denied)
 {
     if (ns == NS_WRITABLE)
     {
         if (*ns_recycle_writable == NULL)
         {
             *ns_recycle_writable = xmlNewNs(xmlDocGetRootElement(i_node->doc),
                                            NS_WRITABLE, ACL_NS_Q_WRITABLE);
         }
         xmlSetNs(i_node, *ns_recycle_writable);
         *ret = pcmk_rc_ok;
     }
     else if (ns == NS_READABLE)
     {
         if (*ns_recycle_readable == NULL)
         {
             *ns_recycle_readable = xmlNewNs(xmlDocGetRootElement(i_node->doc),
                                            NS_READABLE, ACL_NS_Q_READABLE);
         }
         xmlSetNs(i_node, *ns_recycle_readable);
         *ret = pcmk_rc_ok;
     }
     else if (ns == NS_DENIED)
     {
         if (*ns_recycle_denied == NULL)
         {
             *ns_recycle_denied = xmlNewNs(xmlDocGetRootElement(i_node->doc),
                                          NS_DENIED, ACL_NS_Q_DENIED);
         };
         xmlSetNs(i_node, *ns_recycle_denied);
         *ret = pcmk_rc_ok;
     }
 }
 
 /*!
  * \brief Annotate a given XML element or property and its siblings with
  *        XML namespaces to indicate ACL permissions
  *
  * \param[in,out] xml_modify  XML to annotate
  *
  * \return  A standard Pacemaker return code
  *          Namely:
  *          - pcmk_rc_ok upon success,
  *          - pcmk_rc_already if ACLs were not applicable,
  *          - pcmk_rc_schema_validation if the validation schema version
  *              is unsupported (see note), or
  *          - EINVAL or ENOMEM as appropriate;
  *
  * \note This function is recursive
  */
 static int
 annotate_with_siblings(xmlNode *xml_modify)
 {
 
     static xmlNs *ns_recycle_writable = NULL,
                  *ns_recycle_readable = NULL,
                  *ns_recycle_denied = NULL;
     static const xmlDoc *prev_doc = NULL;
 
     xmlNode *i_node = NULL;
     const xmlChar *ns;
     int ret = EINVAL; // nodes have not been processed yet
 
     if (prev_doc == NULL || prev_doc != xml_modify->doc) {
         prev_doc = xml_modify->doc;
         ns_recycle_writable = ns_recycle_readable = ns_recycle_denied = NULL;
     }
 
     for (i_node = xml_modify; i_node != NULL; i_node = i_node->next) {
         switch (i_node->type) {
             case XML_ELEMENT_NODE:
                 pcmk__set_xml_doc_flag(i_node, pcmk__xf_tracking);
 
                 if (!pcmk__check_acl(i_node, NULL, pcmk__xf_acl_read)) {
                     ns = NS_DENIED;
                 } else if (!pcmk__check_acl(i_node, NULL, pcmk__xf_acl_write)) {
                     ns = NS_READABLE;
                 } else {
                     ns = NS_WRITABLE;
                 }
                 pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
                                                    &ns_recycle_writable,
                                                    &ns_recycle_readable,
                                                    &ns_recycle_denied);
                 // @TODO Could replace recursion with iteration to save stack
                 if (i_node->properties != NULL) {
                     /* This is not entirely clear, but relies on the very same
                      * class-hierarchy emulation that libxml2 has firmly baked
                      * in its API/ABI
                      */
                     ret |= annotate_with_siblings((xmlNodePtr)
                                                   i_node->properties);
                 }
                 if (i_node->children != NULL) {
                     ret |= annotate_with_siblings(i_node->children);
                 }
                 break;
 
             case XML_ATTRIBUTE_NODE:
                 // We can utilize that parent has already been assigned the ns
                 if (!pcmk__check_acl(i_node->parent,
                                      (const char *) i_node->name,
                                      pcmk__xf_acl_read)) {
                     ns = NS_DENIED;
                 } else if (!pcmk__check_acl(i_node,
                                        (const char *) i_node->name,
                                        pcmk__xf_acl_write)) {
                     ns = NS_READABLE;
                 } else {
                     ns = NS_WRITABLE;
                 }
                 pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
                                                    &ns_recycle_writable,
                                                    &ns_recycle_readable,
                                                    &ns_recycle_denied);
                 break;
 
             case XML_COMMENT_NODE:
                 // We can utilize that parent has already been assigned the ns
                 if (!pcmk__check_acl(i_node->parent,
                                      (const char *) i_node->name,
                                      pcmk__xf_acl_read)) {
                     ns = NS_DENIED;
                 } else if (!pcmk__check_acl(i_node->parent,
                                             (const char *) i_node->name,
                                             pcmk__xf_acl_write)) {
                     ns = NS_READABLE;
                 } else {
                     ns = NS_WRITABLE;
                 }
                 pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
                                                    &ns_recycle_writable,
                                                    &ns_recycle_readable,
                                                    &ns_recycle_denied);
                 break;
 
             default:
                 break;
         }
     }
 
     return ret;
 }
 
 int
 pcmk__acl_annotate_permissions(const char *cred, const xmlDoc *cib_doc,
                                xmlDoc **acl_evaled_doc)
 {
     int ret;
     xmlNode *target, *comment;
     const char *validation;
 
     CRM_CHECK(cred != NULL, return EINVAL);
     CRM_CHECK(cib_doc != NULL, return EINVAL);
     CRM_CHECK(acl_evaled_doc != NULL, return EINVAL);
 
     /* avoid trivial accidental XML injection */
     if (strpbrk(cred, "<>&") != NULL) {
         return EINVAL;
     }
 
     if (!pcmk_acl_required(cred)) {
         /* nothing to evaluate */
         return pcmk_rc_already;
     }
 
     // @COMPAT xmlDocGetRootElement() requires non-const in libxml2 < 2.9.2
     validation = crm_element_value(xmlDocGetRootElement((xmlDoc *) cib_doc),
                                    PCMK_XA_VALIDATE_WITH);
 
     if (pcmk__cmp_schemas_by_name(PCMK__COMPAT_ACL_2_MIN_INCL,
                                   validation) > 0) {
         return pcmk_rc_schema_validation;
     }
 
     target = pcmk__xml_copy(NULL, xmlDocGetRootElement((xmlDoc *) cib_doc));
     if (target == NULL) {
         return EINVAL;
     }
 
     pcmk__enable_acl(target, target, cred);
 
     ret = annotate_with_siblings(target);
 
     if (ret == pcmk_rc_ok) {
         char *credentials = crm_strdup_printf("ACLs as evaluated for user %s",
                                               cred);
 
         comment = xmlNewDocComment(target->doc, (pcmkXmlStr) credentials);
         free(credentials);
         if (comment == NULL) {
-            xmlFreeNode(target);
+            free_xml(target);
             return EINVAL;
         }
         xmlAddPrevSibling(xmlDocGetRootElement(target->doc), comment);
         *acl_evaled_doc = target->doc;
         return pcmk_rc_ok;
     } else {
-        xmlFreeNode(target);
+        free_xml(target);
         return ret; //for now, it should be some kind of error
     }
 }
 
 int
 pcmk__acl_evaled_render(xmlDoc *annotated_doc, enum pcmk__acl_render_how how,
                         xmlChar **doc_txt_ptr)
 {
     xmlDoc *xslt_doc;
     xsltStylesheet *xslt;
     xsltTransformContext *xslt_ctxt;
     xmlDoc *res;
     char *sfile;
     static const char *params_namespace[] = {
         "accessrendercfg:c-writable",           ACL_NS_Q_PREFIX "writable:",
         "accessrendercfg:c-readable",           ACL_NS_Q_PREFIX "readable:",
         "accessrendercfg:c-denied",             ACL_NS_Q_PREFIX "denied:",
         "accessrendercfg:c-reset",              "",
         "accessrender:extra-spacing",           "no",
         "accessrender:self-reproducing-prefix", ACL_NS_Q_PREFIX,
         NULL
     }, *params_useansi[] = {
         /* start with hard-coded defaults, then adapt per the template ones */
         "accessrendercfg:c-writable",           "\x1b[32m",
         "accessrendercfg:c-readable",           "\x1b[34m",
         "accessrendercfg:c-denied",             "\x1b[31m",
         "accessrendercfg:c-reset",              "\x1b[0m",
         "accessrender:extra-spacing",           "no",
         "accessrender:self-reproducing-prefix", ACL_NS_Q_PREFIX,
         NULL
     }, *params_noansi[] = {
         "accessrendercfg:c-writable",           "vvv---[ WRITABLE ]---vvv",
         "accessrendercfg:c-readable",           "vvv---[ READABLE ]---vvv",
         "accessrendercfg:c-denied",             "vvv---[ ~DENIED~ ]---vvv",
         "accessrendercfg:c-reset",              "",
         "accessrender:extra-spacing",           "yes",
         "accessrender:self-reproducing-prefix", "",
         NULL
     };
     const char **params;
     int rc = pcmk_rc_ok;
     xmlParserCtxtPtr parser_ctxt;
 
     /* unfortunately, the input (coming from CIB originally) was parsed with
        blanks ignored, and since the output is a conversion of XML to text
        format (we would be covered otherwise thanks to implicit
        pretty-printing), we need to dump the tree to string output first,
        only to subsequently reparse it -- this time with blanks honoured */
     xmlChar *annotated_dump;
     int dump_size;
 
     CRM_ASSERT(how != pcmk__acl_render_none);
 
     // Color is the default render mode for terminals; text is default otherwise
     if (how == pcmk__acl_render_default) {
         if (isatty(STDOUT_FILENO)) {
             how = pcmk__acl_render_color;
         } else {
             how = pcmk__acl_render_text;
         }
     }
 
     xmlDocDumpFormatMemory(annotated_doc, &annotated_dump, &dump_size, 1);
     res = xmlReadDoc(annotated_dump, "on-the-fly-access-render", NULL,
                      XML_PARSE_NONET);
     CRM_ASSERT(res != NULL);
     xmlFree(annotated_dump);
     xmlFreeDoc(annotated_doc);
     annotated_doc = res;
 
     sfile = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_base_xslt,
                                     "access-render-2");
     parser_ctxt = xmlNewParserCtxt();
 
     CRM_ASSERT(sfile != NULL);
     pcmk__mem_assert(parser_ctxt);
 
     xslt_doc = xmlCtxtReadFile(parser_ctxt, sfile, NULL, XML_PARSE_NONET);
 
     xslt = xsltParseStylesheetDoc(xslt_doc);  /* acquires xslt_doc! */
     if (xslt == NULL) {
         crm_crit("Problem in parsing %s", sfile);
         rc = EINVAL;
         goto done;
     }
     xmlFreeParserCtxt(parser_ctxt);
 
     xslt_ctxt = xsltNewTransformContext(xslt, annotated_doc);
     pcmk__mem_assert(xslt_ctxt);
 
     switch (how) {
         case pcmk__acl_render_namespace:
             params = params_namespace;
             break;
         case pcmk__acl_render_text:
             params = params_noansi;
             break;
         default:
             /* pcmk__acl_render_color is the only remaining option.
              * The compiler complains about params possibly uninitialized if we
              * don't use default here.
              */
             params = params_useansi;
             break;
     }
 
     xsltQuoteUserParams(xslt_ctxt, params);
 
     res = xsltApplyStylesheetUser(xslt, annotated_doc, NULL,
                                   NULL, NULL, xslt_ctxt);
 
     xmlFreeDoc(annotated_doc);
     annotated_doc = NULL;
     xsltFreeTransformContext(xslt_ctxt);
     xslt_ctxt = NULL;
 
     if (how == pcmk__acl_render_color && params != params_useansi) {
         char **param_i = (char **) params;
         do {
             free(*param_i);
         } while (*param_i++ != NULL);
         free(params);
     }
 
     if (res == NULL) {
         rc = EINVAL;
     } else {
         int doc_txt_len;
         int temp = xsltSaveResultToString(doc_txt_ptr, &doc_txt_len, res, xslt);
         xmlFreeDoc(res);
         if (temp != 0) {
             rc = EINVAL;
         }
     }
 
 done:
     if (xslt != NULL) {
         xsltFreeStylesheet(xslt);
     }
     free(sfile);
     return rc;
 }
diff --git a/tools/crm_resource_print.c b/tools/crm_resource_print.c
index 8678dd7ed1..e8b20189df 100644
--- a/tools/crm_resource_print.c
+++ b/tools/crm_resource_print.c
@@ -1,927 +1,927 @@
 /*
  * 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 <stdint.h>
 
 #include <crm_resource.h>
 #include <crm/common/lists_internal.h>
 #include <crm/common/output.h>
 #include <crm/common/results.h>
 
 #define cons_string(x) x?x:"NA"
 static int
 print_constraint(xmlNode *xml_obj, void *userdata)
 {
     pcmk_scheduler_t *scheduler = (pcmk_scheduler_t *) userdata;
     pcmk__output_t *out = scheduler->priv;
     xmlNode *lifetime = NULL;
     const char *id = crm_element_value(xml_obj, PCMK_XA_ID);
     pcmk_rule_input_t rule_input = {
         .now = scheduler->now,
     };
 
     if (id == NULL) {
         return pcmk_rc_ok;
     }
 
     // @COMPAT PCMK__XE_LIFETIME is deprecated
     lifetime = pcmk__xe_first_child(xml_obj, PCMK__XE_LIFETIME, NULL, NULL);
     if (pcmk__evaluate_rules(lifetime, &rule_input, NULL) != pcmk_rc_ok) {
         return pcmk_rc_ok;
     }
 
     if (!pcmk__xe_is(xml_obj, PCMK_XE_RSC_COLOCATION)) {
         return pcmk_rc_ok;
     }
 
     out->info(out, "Constraint %s %s %s %s %s %s %s",
               xml_obj->name,
               cons_string(crm_element_value(xml_obj, PCMK_XA_ID)),
               cons_string(crm_element_value(xml_obj, PCMK_XA_RSC)),
               cons_string(crm_element_value(xml_obj, PCMK_XA_WITH_RSC)),
               cons_string(crm_element_value(xml_obj, PCMK_XA_SCORE)),
               cons_string(crm_element_value(xml_obj, PCMK_XA_RSC_ROLE)),
               cons_string(crm_element_value(xml_obj, PCMK_XA_WITH_RSC_ROLE)));
 
     return pcmk_rc_ok;
 }
 
 void
 cli_resource_print_cts_constraints(pcmk_scheduler_t *scheduler)
 {
     pcmk__xe_foreach_child(pcmk_find_cib_element(scheduler->input,
                                                  PCMK_XE_CONSTRAINTS),
                            NULL, print_constraint, scheduler);
 }
 
 void
 cli_resource_print_cts(pcmk_resource_t *rsc, pcmk__output_t *out)
 {
     const char *host = NULL;
     bool needs_quorum = TRUE;
     const char *rtype = crm_element_value(rsc->xml, PCMK_XA_TYPE);
     const char *rprov = crm_element_value(rsc->xml, PCMK_XA_PROVIDER);
     const char *rclass = crm_element_value(rsc->xml, PCMK_XA_CLASS);
     pcmk_node_t *node = pcmk__current_node(rsc);
 
     if (pcmk_is_set(rsc->flags, pcmk_rsc_fence_device)) {
         needs_quorum = FALSE;
     } else {
         // @TODO check requires in resource meta-data and rsc_defaults
     }
 
     if (node != NULL) {
         host = node->details->uname;
     }
 
     out->info(out, "Resource: %s %s %s %s %s %s %s %s %d %lld %#.16llx",
               rsc->xml->name, rsc->id,
               rsc->clone_name ? rsc->clone_name : rsc->id, rsc->parent ? rsc->parent->id : "NA",
               rprov ? rprov : "NA", rclass, rtype, host ? host : "NA", needs_quorum, rsc->flags,
               rsc->flags);
 
     g_list_foreach(rsc->children, (GFunc) cli_resource_print_cts, out);
 }
 
 // \return Standard Pacemaker return code
 int
 cli_resource_print_operations(const char *rsc_id, const char *host_uname,
                               bool active, pcmk_scheduler_t *scheduler)
 {
     pcmk__output_t *out = scheduler->priv;
     int rc = pcmk_rc_no_output;
     GList *ops = find_operations(rsc_id, host_uname, active, scheduler);
 
     if (!ops) {
         return rc;
     }
 
     out->begin_list(out, NULL, NULL, "Resource Operations");
     rc = pcmk_rc_ok;
 
     for (GList *lpc = ops; lpc != NULL; lpc = lpc->next) {
         xmlNode *xml_op = (xmlNode *) lpc->data;
         out->message(out, "node-and-op", scheduler, xml_op);
     }
 
     out->end_list(out);
     return rc;
 }
 
 // \return Standard Pacemaker return code
 int
 cli_resource_print(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler,
                    bool expanded)
 {
     pcmk__output_t *out = scheduler->priv;
     uint32_t show_opts = pcmk_show_pending;
     GList *all = NULL;
 
     all = g_list_prepend(all, (gpointer) "*");
 
     out->begin_list(out, NULL, NULL, "Resource Config");
     out->message(out, pcmk__map_element_name(rsc->xml), show_opts, rsc, all,
                  all);
     out->message(out, "resource-config", rsc, !expanded);
     out->end_list(out);
 
     g_list_free(all);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("attribute-changed", "attr_update_data_t *")
 static int
 attribute_changed_default(pcmk__output_t *out, va_list args)
 {
     attr_update_data_t *ud = va_arg(args, attr_update_data_t *);
 
     out->info(out, "Set '%s' option: "
               PCMK_XA_ID "=%s%s%s%s%s value=%s",
               ud->given_rsc_id, ud->found_attr_id,
               ((ud->attr_set_id == NULL)? "" : " " PCMK__XA_SET "="),
               pcmk__s(ud->attr_set_id, ""),
               ((ud->attr_name == NULL)? "" : " " PCMK_XA_NAME "="),
               pcmk__s(ud->attr_name, ""), ud->attr_value);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("attribute-changed", "attr_update_data_t *")
 static int
 attribute_changed_xml(pcmk__output_t *out, va_list args)
 {
     attr_update_data_t *ud = va_arg(args, attr_update_data_t *);
 
     pcmk__output_xml_create_parent(out, (const char *) ud->rsc->xml->name,
                                    PCMK_XA_ID, ud->rsc->id,
                                    NULL);
 
     pcmk__output_xml_create_parent(out, ud->attr_set_type,
                                    PCMK_XA_ID, ud->attr_set_id,
                                    NULL);
 
     pcmk__output_create_xml_node(out, PCMK_XE_NVPAIR,
                                  PCMK_XA_ID, ud->found_attr_id,
                                  PCMK_XA_VALUE, ud->attr_value,
                                  PCMK_XA_NAME, ud->attr_name,
                                  NULL);
 
     pcmk__output_xml_pop_parent(out);
     pcmk__output_xml_pop_parent(out);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("attribute-changed-list", "GList *")
 static int
 attribute_changed_list_default(pcmk__output_t *out, va_list args)
 {
     GList *results = va_arg(args, GList *);
 
     if (results == NULL) {
         return pcmk_rc_no_output;
     }
 
     for (GList *iter = results; iter != NULL; iter = iter->next) {
         attr_update_data_t *ud = iter->data;
         out->message(out, "attribute-changed", ud);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("attribute-changed-list", "GList *")
 static int
 attribute_changed_list_xml(pcmk__output_t *out, va_list args)
 {
     GList *results = va_arg(args, GList *);
 
     if (results == NULL) {
         return pcmk_rc_no_output;
     }
 
     pcmk__output_xml_create_parent(out, PCMK__XE_RESOURCE_SETTINGS, NULL);
 
     for (GList *iter = results; iter != NULL; iter = iter->next) {
         attr_update_data_t *ud = iter->data;
         out->message(out, "attribute-changed", ud);
     }
 
     pcmk__output_xml_pop_parent(out);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("attribute-list", "pcmk_resource_t *", "const char *",
                   "const char *")
 static int
 attribute_list_default(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     const char *attr = va_arg(args, char *);
     const char *value = va_arg(args, const char *);
 
     if (value != NULL) {
         out->begin_list(out, NULL, NULL, "Attributes");
         out->list_item(out, attr, "%s", value);
         out->end_list(out);
         return pcmk_rc_ok;
     } else {
         out->err(out, "Attribute '%s' not found for '%s'", attr, rsc->id);
     }
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("agent-status", "int", "const char *", "const char *", "const char *",
                   "const char *", "const char *", "crm_exit_t", "const char *")
 static int
 agent_status_default(pcmk__output_t *out, va_list args) {
     int status = va_arg(args, int);
     const char *action = va_arg(args, const char *);
     const char *name = va_arg(args, const char *);
     const char *class = va_arg(args, const char *);
     const char *provider = va_arg(args, const char *);
     const char *type = va_arg(args, const char *);
     crm_exit_t rc = va_arg(args, crm_exit_t);
     const char *exit_reason = va_arg(args, const char *);
 
     if (status == PCMK_EXEC_DONE) {
         /* Operation <action> [for <resource>] (<class>[:<provider>]:<agent>)
          * returned <exit-code> (<exit-description>[: <exit-reason>])
          */
         out->info(out, "Operation %s%s%s (%s%s%s:%s) returned %d (%s%s%s)",
                   action,
                   ((name == NULL)? "" : " for "), ((name == NULL)? "" : name),
                   class,
                   ((provider == NULL)? "" : ":"),
                   ((provider == NULL)? "" : provider),
                   type, (int) rc, services_ocf_exitcode_str((int) rc),
                   ((exit_reason == NULL)? "" : ": "),
                   ((exit_reason == NULL)? "" : exit_reason));
     } else {
         /* Operation <action> [for <resource>] (<class>[:<provider>]:<agent>)
          * could not be executed (<execution-status>[: <exit-reason>])
          */
         out->err(out,
                  "Operation %s%s%s (%s%s%s:%s) could not be executed (%s%s%s)",
                  action,
                  ((name == NULL)? "" : " for "), ((name == NULL)? "" : name),
                  class,
                  ((provider == NULL)? "" : ":"),
                  ((provider == NULL)? "" : provider),
                  type, pcmk_exec_status_str(status),
                  ((exit_reason == NULL)? "" : ": "),
                  ((exit_reason == NULL)? "" : exit_reason));
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("agent-status", "int", "const char *", "const char *", "const char *",
                   "const char *", "const char *", "crm_exit_t", "const char *")
 static int
 agent_status_xml(pcmk__output_t *out, va_list args) {
     int status = va_arg(args, int);
     const char *action G_GNUC_UNUSED = va_arg(args, const char *);
     const char *name G_GNUC_UNUSED = va_arg(args, const char *);
     const char *class G_GNUC_UNUSED = va_arg(args, const char *);
     const char *provider G_GNUC_UNUSED = va_arg(args, const char *);
     const char *type G_GNUC_UNUSED = va_arg(args, const char *);
     crm_exit_t rc = va_arg(args, crm_exit_t);
     const char *exit_reason = va_arg(args, const char *);
 
     char *exit_s = pcmk__itoa(rc);
     const char *message = services_ocf_exitcode_str((int) rc);
     char *status_s = pcmk__itoa(status);
     const char *execution_message = pcmk_exec_status_str(status);
 
     pcmk__output_create_xml_node(out, PCMK_XE_AGENT_STATUS,
                                  PCMK_XA_CODE, exit_s,
                                  PCMK_XA_MESSAGE, message,
                                  PCMK_XA_EXECUTION_CODE, status_s,
                                  PCMK_XA_EXECUTION_MESSAGE, execution_message,
                                  PCMK_XA_REASON, exit_reason,
                                  NULL);
 
     free(exit_s);
     free(status_s);
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("attribute-list", "pcmk_resource_t *", "const char *",
                   "const char *")
 static int
 attribute_list_text(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     const char *attr = va_arg(args, char *);
     const char *value = va_arg(args, const char *);
 
     if (value != NULL) {
         pcmk__formatted_printf(out, "%s\n", value);
         return pcmk_rc_ok;
     } else {
         out->err(out, "Attribute '%s' not found for '%s'", attr, rsc->id);
     }
     return pcmk_rc_ok;
 }
 PCMK__OUTPUT_ARGS("override", "const char *", "const char *", "const char *")
 static int
 override_default(pcmk__output_t *out, va_list args) {
     const char *rsc_name = va_arg(args, const char *);
     const char *name = va_arg(args, const char *);
     const char *value = va_arg(args, const char *);
 
     if (rsc_name == NULL) {
         out->list_item(out, NULL, "Overriding the cluster configuration with '%s' = '%s'",
                        name, value);
     } else {
         out->list_item(out, NULL, "Overriding the cluster configuration for '%s' with '%s' = '%s'",
                        rsc_name, name, value);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("override", "const char *", "const char *", "const char *")
 static int
 override_xml(pcmk__output_t *out, va_list args) {
     const char *rsc_name = va_arg(args, const char *);
     const char *name = va_arg(args, const char *);
     const char *value = va_arg(args, const char *);
 
     xmlNodePtr node = pcmk__output_create_xml_node(out, PCMK_XE_OVERRIDE,
                                                    PCMK_XA_NAME, name,
                                                    PCMK_XA_VALUE, value,
                                                    NULL);
 
     if (rsc_name != NULL) {
         crm_xml_add(node, PCMK_XA_RSC, rsc_name);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("property-list", "pcmk_resource_t *", "const char *")
 static int
 property_list_default(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     const char *attr = va_arg(args, char *);
 
     const char *value = crm_element_value(rsc->xml, attr);
 
     if (value != NULL) {
         out->begin_list(out, NULL, NULL, "Properties");
         out->list_item(out, attr, "%s", value);
         out->end_list(out);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("property-list", "pcmk_resource_t *", "const char *")
 static int
 property_list_text(pcmk__output_t *out, va_list args) {
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     const char *attr = va_arg(args, const char *);
 
     const char *value = crm_element_value(rsc->xml, attr);
 
     if (value != NULL) {
         pcmk__formatted_printf(out, "%s\n", value);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("resource-agent-action", "int", "const char *", "const char *",
                   "const char *", "const char *", "const char *", "GHashTable *",
                   "crm_exit_t", "int", "const char *", "const char *", "const char *")
 static int
 resource_agent_action_default(pcmk__output_t *out, va_list args) {
     int verbose = va_arg(args, int);
 
     const char *class = va_arg(args, const char *);
     const char *provider = va_arg(args, const char *);
     const char *type = va_arg(args, const char *);
     const char *rsc_name = va_arg(args, const char *);
     const char *action = va_arg(args, const char *);
     GHashTable *overrides = va_arg(args, GHashTable *);
     crm_exit_t rc = va_arg(args, crm_exit_t);
     int status = va_arg(args, int);
     const char *exit_reason = va_arg(args, const char *);
     const char *stdout_data = va_arg(args, const char *);
     const char *stderr_data = va_arg(args, const char *);
 
     if (overrides) {
         GHashTableIter iter;
         const char *name = NULL;
         const char *value = NULL;
 
         out->begin_list(out, NULL, NULL, PCMK_XE_OVERRIDES);
 
         g_hash_table_iter_init(&iter, overrides);
         while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &value)) {
             out->message(out, "override", rsc_name, name, value);
         }
 
         out->end_list(out);
     }
 
     out->message(out, "agent-status", status, action, rsc_name, class, provider,
                  type, rc, exit_reason);
 
     /* hide output for validate-all if not in verbose */
     if ((verbose == 0)
         && pcmk__str_eq(action, PCMK_ACTION_VALIDATE_ALL, pcmk__str_casei)) {
         return pcmk_rc_ok;
     }
 
     if (stdout_data || stderr_data) {
         xmlNodePtr doc = NULL;
 
         if (stdout_data != NULL) {
             doc = pcmk__xml_parse(stdout_data);
         }
         if (doc != NULL) {
             out->output_xml(out, PCMK_XE_COMMAND, stdout_data);
-            xmlFreeNode(doc);
+            free_xml(doc);
         } else {
             out->subprocess_output(out, rc, stdout_data, stderr_data);
         }
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("resource-agent-action", "int", "const char *", "const char *",
                   "const char *", "const char *", "const char *", "GHashTable *",
                   "crm_exit_t", "int", "const char *", "const char *", "const char *")
 static int
 resource_agent_action_xml(pcmk__output_t *out, va_list args) {
     int verbose G_GNUC_UNUSED = va_arg(args, int);
 
     const char *class = va_arg(args, const char *);
     const char *provider = va_arg(args, const char *);
     const char *type = va_arg(args, const char *);
     const char *rsc_name = va_arg(args, const char *);
     const char *action = va_arg(args, const char *);
     GHashTable *overrides = va_arg(args, GHashTable *);
     crm_exit_t rc = va_arg(args, crm_exit_t);
     int status = va_arg(args, int);
     const char *exit_reason = va_arg(args, const char *);
     const char *stdout_data = va_arg(args, const char *);
     const char *stderr_data = va_arg(args, const char *);
 
     xmlNodePtr node = NULL;
 
     node = pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCE_AGENT_ACTION,
                                           PCMK_XA_ACTION, action,
                                           PCMK_XA_CLASS, class,
                                           PCMK_XA_TYPE, type,
                                           NULL);
 
     if (rsc_name) {
         crm_xml_add(node, PCMK_XA_RSC, rsc_name);
     }
 
     crm_xml_add(node, PCMK_XA_PROVIDER, provider);
 
     if (overrides) {
         GHashTableIter iter;
         const char *name = NULL;
         const char *value = NULL;
 
         out->begin_list(out, NULL, NULL, PCMK_XE_OVERRIDES);
 
         g_hash_table_iter_init(&iter, overrides);
         while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &value)) {
             out->message(out, "override", rsc_name, name, value);
         }
 
         out->end_list(out);
     }
 
     out->message(out, "agent-status", status, action, rsc_name, class, provider,
                  type, rc, exit_reason);
 
     if (stdout_data || stderr_data) {
         xmlNodePtr doc = NULL;
 
         if (stdout_data != NULL) {
             doc = pcmk__xml_parse(stdout_data);
         }
         if (doc != NULL) {
             out->output_xml(out, PCMK_XE_COMMAND, stdout_data);
-            xmlFreeNode(doc);
+            free_xml(doc);
         } else {
             out->subprocess_output(out, rc, stdout_data, stderr_data);
         }
     }
 
     pcmk__output_xml_pop_parent(out);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("resource-check-list", "resource_checks_t *")
 static int
 resource_check_list_default(pcmk__output_t *out, va_list args) {
     resource_checks_t *checks = va_arg(args, resource_checks_t *);
 
     const pcmk_resource_t *parent = pe__const_top_resource(checks->rsc, false);
 
     if (checks->flags == 0) {
         return pcmk_rc_no_output;
     }
 
     out->begin_list(out, NULL, NULL, "Resource Checks");
 
     if (pcmk_is_set(checks->flags, rsc_remain_stopped)) {
         out->list_item(out, "check", "Configuration specifies '%s' should remain stopped",
                        parent->id);
     }
 
     if (pcmk_is_set(checks->flags, rsc_unpromotable)) {
         out->list_item(out, "check", "Configuration specifies '%s' should not be promoted",
                        parent->id);
     }
 
     if (pcmk_is_set(checks->flags, rsc_unmanaged)) {
         out->list_item(out, "check", "Configuration prevents cluster from stopping or starting unmanaged '%s'",
                        parent->id);
     }
 
     if (pcmk_is_set(checks->flags, rsc_locked)) {
         out->list_item(out, "check", "'%s' is locked to node %s due to shutdown",
                        parent->id, checks->lock_node);
     }
 
     if (pcmk_is_set(checks->flags, rsc_node_health)) {
         out->list_item(out, "check",
                        "'%s' cannot run on unhealthy nodes due to "
                        PCMK_OPT_NODE_HEALTH_STRATEGY "='%s'",
                        parent->id,
                        pcmk__cluster_option(checks->rsc->cluster->config_hash,
                                             PCMK_OPT_NODE_HEALTH_STRATEGY));
     }
 
     out->end_list(out);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("resource-check-list", "resource_checks_t *")
 static int
 resource_check_list_xml(pcmk__output_t *out, va_list args) {
     resource_checks_t *checks = va_arg(args, resource_checks_t *);
 
     const pcmk_resource_t *parent = pe__const_top_resource(checks->rsc, false);
 
     xmlNodePtr node = pcmk__output_create_xml_node(out, PCMK_XE_CHECK,
                                                    PCMK_XA_ID, parent->id,
                                                    NULL);
 
     if (pcmk_is_set(checks->flags, rsc_remain_stopped)) {
         pcmk__xe_set_bool_attr(node, PCMK_XA_REMAIN_STOPPED, true);
     }
 
     if (pcmk_is_set(checks->flags, rsc_unpromotable)) {
         pcmk__xe_set_bool_attr(node, PCMK_XA_PROMOTABLE, false);
     }
 
     if (pcmk_is_set(checks->flags, rsc_unmanaged)) {
         pcmk__xe_set_bool_attr(node, PCMK_XA_UNMANAGED, true);
     }
 
     if (pcmk_is_set(checks->flags, rsc_locked)) {
         crm_xml_add(node, PCMK_XA_LOCKED_TO_HYPHEN, checks->lock_node);
     }
 
     if (pcmk_is_set(checks->flags, rsc_node_health)) {
         pcmk__xe_set_bool_attr(node, PCMK_XA_UNHEALTHY, true);
     }
 
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("resource-search-list", "GList *", "const gchar *")
 static int
 resource_search_list_default(pcmk__output_t *out, va_list args)
 {
     GList *nodes = va_arg(args, GList *);
     const gchar *requested_name = va_arg(args, const gchar *);
 
     bool printed = false;
     int rc = pcmk_rc_no_output;
 
     if (!out->is_quiet(out) && nodes == NULL) {
         out->err(out, "resource %s is NOT running", requested_name);
         return rc;
     }
 
     for (GList *lpc = nodes; lpc != NULL; lpc = lpc->next) {
         node_info_t *ni = (node_info_t *) lpc->data;
 
         if (!printed) {
             out->begin_list(out, NULL, NULL, "Nodes");
             printed = true;
             rc = pcmk_rc_ok;
         }
 
         if (out->is_quiet(out)) {
             out->list_item(out, "node", "%s", ni->node_name);
         } else {
             const char *role_text = "";
 
             if (ni->promoted) {
 #ifdef PCMK__COMPAT_2_0
                 role_text = " " PCMK__ROLE_PROMOTED_LEGACY;
 #else
                 role_text = " " PCMK_ROLE_PROMOTED;
 #endif
             }
             out->list_item(out, "node", "resource %s is running on: %s%s",
                            requested_name, ni->node_name, role_text);
         }
     }
 
     if (printed) {
         out->end_list(out);
     }
 
     return rc;
 }
 
 PCMK__OUTPUT_ARGS("resource-search-list", "GList *", "const gchar *")
 static int
 resource_search_list_xml(pcmk__output_t *out, va_list args)
 {
     GList *nodes = va_arg(args, GList *);
     const gchar *requested_name = va_arg(args, const gchar *);
 
     pcmk__output_xml_create_parent(out, PCMK_XE_NODES,
                                    PCMK_XA_RESOURCE, requested_name,
                                    NULL);
 
     for (GList *lpc = nodes; lpc != NULL; lpc = lpc->next) {
         node_info_t *ni = (node_info_t *) lpc->data;
         xmlNodePtr sub_node = pcmk__output_create_xml_text_node(out,
                                                                 PCMK_XE_NODE,
                                                                 ni->node_name);
 
         if (ni->promoted) {
             crm_xml_add(sub_node, PCMK_XA_STATE, "promoted");
         }
     }
 
     pcmk__output_xml_pop_parent(out);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("resource-reasons-list", "GList *", "pcmk_resource_t *",
                   "pcmk_node_t *")
 static int
 resource_reasons_list_default(pcmk__output_t *out, va_list args)
 {
     GList *resources = va_arg(args, GList *);
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     pcmk_node_t *node = va_arg(args, pcmk_node_t *);
 
     const char *host_uname = (node == NULL)? NULL : node->details->uname;
 
     out->begin_list(out, NULL, NULL, "Resource Reasons");
 
     if ((rsc == NULL) && (host_uname == NULL)) {
         GList *lpc = NULL;
         GList *hosts = NULL;
 
         for (lpc = resources; lpc != NULL; lpc = lpc->next) {
             pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data;
             rsc->fns->location(rsc, &hosts, TRUE);
 
             if (hosts == NULL) {
                 out->list_item(out, "reason", "Resource %s is not running", rsc->id);
             } else {
                 out->list_item(out, "reason", "Resource %s is running", rsc->id);
             }
 
             cli_resource_check(out, rsc, NULL);
             g_list_free(hosts);
             hosts = NULL;
         }
 
     } else if ((rsc != NULL) && (host_uname != NULL)) {
         if (resource_is_running_on(rsc, host_uname)) {
             out->list_item(out, "reason", "Resource %s is running on host %s",
                            rsc->id, host_uname);
         } else {
             out->list_item(out, "reason", "Resource %s is not running on host %s",
                            rsc->id, host_uname);
         }
 
         cli_resource_check(out, rsc, node);
 
     } else if ((rsc == NULL) && (host_uname != NULL)) {
         const char* host_uname =  node->details->uname;
         GList *allResources = node->details->allocated_rsc;
         GList *activeResources = node->details->running_rsc;
         GList *unactiveResources = pcmk__subtract_lists(allResources, activeResources, (GCompareFunc) strcmp);
         GList *lpc = NULL;
 
         for (lpc = activeResources; lpc != NULL; lpc = lpc->next) {
             pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data;
             out->list_item(out, "reason", "Resource %s is running on host %s",
                            rsc->id, host_uname);
             cli_resource_check(out, rsc, node);
         }
 
         for(lpc = unactiveResources; lpc != NULL; lpc = lpc->next) {
             pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data;
             out->list_item(out, "reason", "Resource %s is assigned to host %s but not running",
                            rsc->id, host_uname);
             cli_resource_check(out, rsc, node);
         }
 
         g_list_free(allResources);
         g_list_free(activeResources);
         g_list_free(unactiveResources);
 
     } else if ((rsc != NULL) && (host_uname == NULL)) {
         GList *hosts = NULL;
 
         rsc->fns->location(rsc, &hosts, TRUE);
         out->list_item(out, "reason", "Resource %s is %srunning",
                        rsc->id, (hosts? "" : "not "));
         cli_resource_check(out, rsc, NULL);
         g_list_free(hosts);
     }
 
     out->end_list(out);
     return pcmk_rc_ok;
 }
 
 PCMK__OUTPUT_ARGS("resource-reasons-list", "GList *", "pcmk_resource_t *",
                   "pcmk_node_t *")
 static int
 resource_reasons_list_xml(pcmk__output_t *out, va_list args)
 {
     GList *resources = va_arg(args, GList *);
     pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *);
     pcmk_node_t *node = va_arg(args, pcmk_node_t *);
 
     const char *host_uname = (node == NULL)? NULL : node->details->uname;
 
     xmlNodePtr xml_node = pcmk__output_xml_create_parent(out, PCMK_XE_REASON,
                                                          NULL);
 
     if ((rsc == NULL) && (host_uname == NULL)) {
         GList *lpc = NULL;
         GList *hosts = NULL;
 
         pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCES, NULL);
 
         for (lpc = resources; lpc != NULL; lpc = lpc->next) {
             pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data;
             const char *running = NULL;
 
             rsc->fns->location(rsc, &hosts, TRUE);
             running = pcmk__btoa(hosts != NULL);
 
             pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCE,
                                            PCMK_XA_ID, rsc->id,
                                            PCMK_XA_RUNNING, running,
                                            NULL);
 
             cli_resource_check(out, rsc, NULL);
             pcmk__output_xml_pop_parent(out);
             g_list_free(hosts);
             hosts = NULL;
         }
 
         pcmk__output_xml_pop_parent(out);
 
     } else if ((rsc != NULL) && (host_uname != NULL)) {
         if (resource_is_running_on(rsc, host_uname)) {
             crm_xml_add(xml_node, PCMK_XA_RUNNING_ON, host_uname);
         }
 
         cli_resource_check(out, rsc, node);
 
     } else if ((rsc == NULL) && (host_uname != NULL)) {
         const char* host_uname =  node->details->uname;
         GList *allResources = node->details->allocated_rsc;
         GList *activeResources = node->details->running_rsc;
         GList *unactiveResources = pcmk__subtract_lists(allResources, activeResources, (GCompareFunc) strcmp);
         GList *lpc = NULL;
 
         pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCES, NULL);
 
         for (lpc = activeResources; lpc != NULL; lpc = lpc->next) {
             pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data;
 
             pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCE,
                                            PCMK_XA_ID, rsc->id,
                                            PCMK_XA_RUNNING, PCMK_VALUE_TRUE,
                                            PCMK_XA_HOST, host_uname,
                                            NULL);
 
             cli_resource_check(out, rsc, node);
             pcmk__output_xml_pop_parent(out);
         }
 
         for(lpc = unactiveResources; lpc != NULL; lpc = lpc->next) {
             pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data;
 
             pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCE,
                                            PCMK_XA_ID, rsc->id,
                                            PCMK_XA_RUNNING, PCMK_VALUE_FALSE,
                                            PCMK_XA_HOST, host_uname,
                                            NULL);
 
             cli_resource_check(out, rsc, node);
             pcmk__output_xml_pop_parent(out);
         }
 
         pcmk__output_xml_pop_parent(out);
         g_list_free(allResources);
         g_list_free(activeResources);
         g_list_free(unactiveResources);
 
     } else if ((rsc != NULL) && (host_uname == NULL)) {
         GList *hosts = NULL;
 
         rsc->fns->location(rsc, &hosts, TRUE);
         crm_xml_add(xml_node, PCMK_XA_RUNNING, pcmk__btoa(hosts != NULL));
         cli_resource_check(out, rsc, NULL);
         g_list_free(hosts);
     }
 
     pcmk__output_xml_pop_parent(out);
     return pcmk_rc_ok;
 }
 
 static void
 add_resource_name(pcmk_resource_t *rsc, pcmk__output_t *out)
 {
     if (rsc->children == NULL) {
         /* Sometimes PCMK_XE_RESOURCE might act as a PCMK_XA_NAME instead of an
          * XML element name, depending on whether pcmk__output_enable_list_element
          * was called.
          */
         out->list_item(out, PCMK_XE_RESOURCE, "%s", rsc->id);
     } else {
         g_list_foreach(rsc->children, (GFunc) add_resource_name, out);
     }
 }
 
 PCMK__OUTPUT_ARGS("resource-names-list", "GList *")
 static int
 resource_names(pcmk__output_t *out, va_list args) {
     GList *resources = va_arg(args, GList *);
 
     if (resources == NULL) {
         out->err(out, "NO resources configured\n");
         return pcmk_rc_no_output;
     }
 
     out->begin_list(out, NULL, NULL, "Resource Names");
     g_list_foreach(resources, (GFunc) add_resource_name, out);
     out->end_list(out);
     return pcmk_rc_ok;
 }
 
 static pcmk__message_entry_t fmt_functions[] = {
     { "agent-status", "default", agent_status_default },
     { "agent-status", "xml", agent_status_xml },
     { "attribute-changed", "default", attribute_changed_default },
     { "attribute-changed", "xml", attribute_changed_xml },
     { "attribute-changed-list", "default", attribute_changed_list_default },
     { "attribute-changed-list", "xml", attribute_changed_list_xml },
     { "attribute-list", "default", attribute_list_default },
     { "attribute-list", "text", attribute_list_text },
     { "override", "default", override_default },
     { "override", "xml", override_xml },
     { "property-list", "default", property_list_default },
     { "property-list", "text", property_list_text },
     { "resource-agent-action", "default", resource_agent_action_default },
     { "resource-agent-action", "xml", resource_agent_action_xml },
     { "resource-check-list", "default", resource_check_list_default },
     { "resource-check-list", "xml", resource_check_list_xml },
     { "resource-search-list", "default", resource_search_list_default },
     { "resource-search-list", "xml", resource_search_list_xml },
     { "resource-reasons-list", "default", resource_reasons_list_default },
     { "resource-reasons-list", "xml", resource_reasons_list_xml },
     { "resource-names-list", "default", resource_names },
 
     { NULL, NULL, NULL }
 };
 
 void
 crm_resource_register_messages(pcmk__output_t *out) {
     pcmk__register_messages(out, fmt_functions);
 }