diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am
index 8d723a23ae..68a8e9ab5c 100644
--- a/lib/common/Makefile.am
+++ b/lib/common/Makefile.am
@@ -1,109 +1,112 @@
 #
 # Copyright 2004-2022 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
 # This source code is licensed under the GNU General Public License version 2
 # or later (GPLv2+) WITHOUT ANY WARRANTY.
 #
 include $(top_srcdir)/mk/common.mk
 
 AM_CPPFLAGS		+= -I$(top_builddir)/lib/gnu -I$(top_srcdir)/lib/gnu
 
 ## libraries
 lib_LTLIBRARIES	= libcrmcommon.la
 check_LTLIBRARIES = libcrmcommon_test.la
 
 # Disable -Wcast-qual if used, because we do some hacky casting,
 # and because libxml2 has some signatures that should be const but aren't
 # for backward compatibility reasons.
 
 # s390 needs -fPIC 
 # s390-suse-linux/bin/ld: .libs/ipc.o: relocation R_390_PC32DBL against `__stack_chk_fail@@GLIBC_2.4' can not be used when making a shared object; recompile with -fPIC
 
 CFLAGS		= $(CFLAGS_COPY:-Wcast-qual=) -fPIC
 
 # Without "." here, check-recursive will run through the subdirectories first
 # and then run "make check" here.  This will fail, because there's things in
 # the subdirectories that need check_LTLIBRARIES built first.  Adding "." here
 # changes the order so the subdirectories are processed afterwards.
 SUBDIRS = . tests
 
 noinst_HEADERS		= crmcommon_private.h mock_private.h
 
 libcrmcommon_la_LDFLAGS	= -version-info 43:1:9
 
 libcrmcommon_la_CFLAGS	= $(CFLAGS_HARDENED_LIB)
 libcrmcommon_la_LDFLAGS	+= $(LDFLAGS_HARDENED_LIB)
 
 libcrmcommon_la_LIBADD	= @LIBADD_DL@ $(top_builddir)/lib/gnu/libgnu.la
 
 # If configured with --with-profiling or --with-coverage, BUILD_PROFILING will
 # be set and -fno-builtin will be added to the CFLAGS.  However, libcrmcommon
 # uses the fabs() function which is normally supplied by gcc as one of its
 # builtins.  Therefore we need to explicitly link against libm here or the
 # tests won't link.
 if BUILD_PROFILING
 libcrmcommon_la_LIBADD	+= -lm
 endif
 
 # Use += rather than backlashed continuation lines for parsing by bumplibs
 libcrmcommon_la_SOURCES	=
 libcrmcommon_la_SOURCES	+= acl.c
 libcrmcommon_la_SOURCES	+= agents.c
 libcrmcommon_la_SOURCES	+= alerts.c
 libcrmcommon_la_SOURCES	+= attrs.c
 libcrmcommon_la_SOURCES	+= cib.c
 if BUILD_CIBSECRETS
 libcrmcommon_la_SOURCES	+= cib_secrets.c
 endif
 libcrmcommon_la_SOURCES	+= cmdline.c
 libcrmcommon_la_SOURCES	+= digest.c
 libcrmcommon_la_SOURCES	+= health.c
 libcrmcommon_la_SOURCES	+= io.c
 libcrmcommon_la_SOURCES	+= ipc_attrd.c
 libcrmcommon_la_SOURCES	+= ipc_client.c
 libcrmcommon_la_SOURCES	+= ipc_common.c
 libcrmcommon_la_SOURCES	+= ipc_controld.c
 libcrmcommon_la_SOURCES	+= ipc_pacemakerd.c
 libcrmcommon_la_SOURCES 	+= ipc_schedulerd.c
 libcrmcommon_la_SOURCES	+= ipc_server.c
 libcrmcommon_la_SOURCES	+= iso8601.c
 libcrmcommon_la_SOURCES	+= lists.c
 libcrmcommon_la_SOURCES	+= logging.c
 libcrmcommon_la_SOURCES	+= mainloop.c
 libcrmcommon_la_SOURCES	+= messages.c
 libcrmcommon_la_SOURCES	+= nvpair.c
 libcrmcommon_la_SOURCES	+= operations.c
 libcrmcommon_la_SOURCES	+= options.c
 libcrmcommon_la_SOURCES	+= output.c
 libcrmcommon_la_SOURCES	+= output_html.c
 libcrmcommon_la_SOURCES	+= output_log.c
 libcrmcommon_la_SOURCES	+= output_none.c
 libcrmcommon_la_SOURCES	+= output_text.c
 libcrmcommon_la_SOURCES	+= output_xml.c
 libcrmcommon_la_SOURCES	+= patchset.c
 libcrmcommon_la_SOURCES	+= pid.c
 libcrmcommon_la_SOURCES	+= procfs.c
 libcrmcommon_la_SOURCES	+= remote.c
 libcrmcommon_la_SOURCES	+= results.c
 libcrmcommon_la_SOURCES	+= schemas.c
 libcrmcommon_la_SOURCES	+= scores.c
 libcrmcommon_la_SOURCES	+= strings.c
 libcrmcommon_la_SOURCES	+= utils.c
 libcrmcommon_la_SOURCES	+= watchdog.c
 libcrmcommon_la_SOURCES	+= xml.c
 libcrmcommon_la_SOURCES	+= xpath.c
 
-WRAPPED = calloc getenv getpwnam_r uname setgrent getgrent endgrent
-WRAPPED_FLAGS = $(foreach fn,$(WRAPPED),-Wl,--wrap=$(fn))
+#
+# libcrmcommon_test is used only with unit tests, so we can mock system calls.
+# See mock.c for details.
+#
 
 libcrmcommon_test_la_SOURCES	= $(libcrmcommon_la_SOURCES)
 libcrmcommon_test_la_SOURCES	+= mock.c
-libcrmcommon_test_la_LDFLAGS	= $(LDFLAGS_HARDENED_LIB) $(WRAPPED_FLAGS)
+libcrmcommon_test_la_LDFLAGS	= $(LDFLAGS_HARDENED_LIB)
 libcrmcommon_test_la_CFLAGS	= $(libcrmcommon_la_CFLAGS)
-libcrmcommon_test_la_LIBADD	= $(libcrmcommon_la_LIBADD)
+libcrmcommon_test_la_LIBADD	= $(libcrmcommon_la_LIBADD) -lcmocka
+
 nodist_libcrmcommon_test_la_SOURCES = $(nodist_libcrmcommon_la_SOURCES)
 
 clean-generic:
 	rm -f *.log *.debug *.xml *~
diff --git a/lib/common/mock.c b/lib/common/mock.c
index fa9431e6dc..139685a759 100644
--- a/lib/common/mock.c
+++ b/lib/common/mock.c
@@ -1,93 +1,216 @@
 /*
  * Copyright 2021-2022 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 <errno.h>
 #include <pwd.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
 #include <stdlib.h>
+#include <string.h>
+#include <setjmp.h>
 #include <sys/types.h>
 #include <sys/utsname.h>
 #include <grp.h>
 
+#include <cmocka.h>
 #include "mock_private.h"
 
 /* This file is only used when running "make check".  It is built into
  * libcrmcommon_test.a, not into libcrmcommon.so.  It is used to support
  * constructing mock versions of library functions for unit testing.
  *
- * Each unit test will only ever want to use a mocked version of a few
- * library functions (i.e. not all of them). However, we need to mark all
- * the mocked functions as wrapped (with -Wl,--wrap= in the LDFLAGS) in
- * libcrmcommon_test.a so that all those unit tests can share the same
- * special test library.  The unit test then defines its own wrapped
- * function. Because a unit test won't define every single wrapped
- * function, there will be undefined references at link time.
+ * HOW TO ADD A MOCKED FUNCTION:
  *
- * This file takes care of those undefined references.  It defines a
- * wrapped version of every function that simply calls the real libc
- * version.  These wrapped versions are defined with a weak attribute,
- * which means the unit tests can define another wrapped version for
- * unit testing that will override the version defined here.
+ * - In this file, declare a bool pcmk__mock_X variable, and define a __wrap_X
+ *   function with the same prototype as the actual function that performs the
+ *   desired behavior if pcmk__mock_X is true and calls __real_X otherwise.
+ *   You can use cmocka's mock_type() and mock_ptr_type() to pass extra
+ *   information to the mocked function (see existing examples for details).
  *
- * HOW TO ADD A MOCKED FUNCTION:
+ * - In mock_private.h, add declarations for extern bool pcmk__mock_X and the
+ *   __real_X and __wrap_X function prototypes.
  *
- * - Define a __wrap_X function here below with the same prototype as the
- *   actual function and that just calls __real_X.
- * - Add a __real_X and __wrap_X function prototype to mock_private.h.
- * - Add the function name to the WRAPPED variable in Makefile.am.
+ * - In mk/tap.mk, add the function name to the WRAPPED variable.
  *
  * HOW TO USE A MOCKED FUNCTION:
  *
- * - In the Makefile.am for your new test, add:
+ * - #include "mock_private.h" in your test file.
  *
- *   your_fn_test_LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la -lcmocka
- *   your_fn_test_LDFLAGS = -Wl,--wrap=X
+ * - Write your test cases using pcmk__mock_X and cmocka's will_return() as
+ *   needed per the comments for the mocked function below. See existing test
+ *   cases for examples.
+ */
+
+
+/* calloc()
  *
- *   You can use multiple wrapped functions by adding multiple -Wl
- *   arguments.
- * - #include "mock_private.h" in your test file.
- * - Add a __wrap_X function with the same prototype as the real function.
- * - Write your test cases, using will_return(), mock_type(), and
- *   mock_ptr_type() from cmocka.  See existing test cases for details.
+ * If pcmk__mock_calloc is set to true, later calls to malloc() will return
+ * NULL.
  */
 
-void *__attribute__((weak))
-__wrap_calloc(size_t nmemb, size_t size) {
-    return __real_calloc(nmemb, size);
-}
+bool pcmk__mock_calloc = false;
 
-char *__attribute__((weak))
-__wrap_getenv(const char *name) {
-    return __real_getenv(name);
+void *
+__wrap_calloc(size_t nmemb, size_t size)
+{
+    return pcmk__mock_calloc? NULL : __real_calloc(nmemb, size);
 }
 
-int __attribute__((weak))
-__wrap_getpwnam_r(const char *name, struct passwd *pwd,
-                  char *buf, size_t buflen, struct passwd **result) {
-    return __real_getpwnam_r(name, pwd, buf, buflen, result);
-}
 
-int __attribute__((weak))
-__wrap_uname(struct utsname *buf) {
-    return __real_uname(buf);
+/* getenv()
+ *
+ * If pcmk__mock_getenv is set to true, later calls to getenv() must be preceded
+ * by:
+ *
+ *     will_return(__wrap_getenv, return_value);
+ */
+
+bool pcmk__mock_getenv = false;
+
+char *
+__wrap_getenv(const char *name)
+{
+    return pcmk__mock_getenv? mock_ptr_type(char *) : __real_getenv(name);
 }
 
-void __attribute__((weak))
+
+/* setgrent(), getgrent() and endgrent()
+ *
+ * If pcmk__mock_grent is set to true, getgrent() will behave as if the only
+ * groups on the system are:
+ *
+ * - grp0 (user0, user1)
+ * - grp1 (user1)
+ * - grp2 (user2, user1)
+ */
+
+bool pcmk__mock_grent = false;
+
+// Index of group that will be returned next from getgrent()
+static int group_idx = 0;
+
+// Data used for testing
+static const char* grp0_members[] = {
+    "user0", "user1", NULL
+};
+
+static const char* grp1_members[] = {
+    "user1", NULL
+};
+
+static const char* grp2_members[] = {
+    "user2", "user1", NULL
+};
+
+/* An array of "groups" (a struct from grp.h)
+ *
+ * The members of the groups are initalized here to some testing data, casting
+ * away the consts to make the compiler happy and simplify initialization. We
+ * never actually change these variables during the test!
+ *
+ * string literal = const char* (cannot be changed b/c ? )
+ *                  vs. char* (it's getting casted to this)
+ */
+static const int NUM_GROUPS = 3;
+static struct group groups[] = {
+    {(char*)"grp0", (char*)"", 0, (char**)grp0_members},
+    {(char*)"grp1", (char*)"", 1, (char**)grp1_members},
+    {(char*)"grp2", (char*)"", 2, (char**)grp2_members},
+};
+
+// This function resets the group_idx to 0.
+void
 __wrap_setgrent(void) {
-    __real_setgrent();
+    if (pcmk__mock_grent) {
+        group_idx = 0;
+    } else {
+        __real_setgrent();
+    }
 }
 
-struct group * __attribute__((weak))
+/* This function returns the next group entry in the list of groups, or
+ * NULL if there aren't any left.
+ * group_idx is a global variable which keeps track of where you are in the list
+ */
+struct group *
 __wrap_getgrent(void) {
-    return __real_getgrent();
+    if (pcmk__mock_grent) {
+        if (group_idx >= NUM_GROUPS) {
+            return NULL;
+        }
+        return &groups[group_idx++];
+    } else {
+        return __real_getgrent();
+    }
 }
 
-void __attribute__((weak))
+void
 __wrap_endgrent(void) {
-    __real_endgrent();
+    if (!pcmk__mock_grent) {
+        __real_endgrent();
+    }
+}
+
+
+/* getpwnam_r()
+ *
+ * If pcmk__mock_getpwnam_r is set to true, later calls to getpwnam_r() must be
+ * preceded by:
+ *
+ *     will_return(__wrap_getpwnam_r, return_value);
+ *     will_return(__wrap_getpwnam_r, ptr_to_result_struct);
+ */
+
+bool pcmk__mock_getpwnam_r = false;
+
+int
+__wrap_getpwnam_r(const char *name, struct passwd *pwd, char *buf,
+                  size_t buflen, struct passwd **result)
+{
+    if (pcmk__mock_getpwnam_r) {
+        int retval = mock_type(int);
+
+        *result = mock_ptr_type(struct passwd *);
+        return retval;
+
+    } else {
+        return __real_getpwnam_r(name, pwd, buf, buflen, result);
+    }
 }
 
+
+/* uname()
+ *
+ * If pcmk__mock_uname is set to true, later calls to uname() must be preceded
+ * by:
+ *
+ *     will_return(__wrap_uname, return_value);
+ *     will_return(__wrap_uname, node_name_for_buf_parameter_to_uname);
+ */
+
+bool pcmk__mock_uname = false;
+
+int
+__wrap_uname(struct utsname *buf)
+{
+    if (pcmk__mock_uname) {
+        int retval = mock_type(int);
+        char *result = mock_ptr_type(char *);
+
+        if (result != NULL) {
+            strcpy(buf->nodename, result);
+        }
+        return retval;
+
+    } else {
+        return __real_uname(buf);
+    }
+}
diff --git a/lib/common/mock_private.h b/lib/common/mock_private.h
index 0c1134cc3a..c97e3ffb06 100644
--- a/lib/common/mock_private.h
+++ b/lib/common/mock_private.h
@@ -1,45 +1,48 @@
 /*
  * Copyright 2021-2022 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef MOCK_PRIVATE__H
 #  define MOCK_PRIVATE__H
 
 #include <pwd.h>
+#include <stdbool.h>
 #include <stdlib.h>
 #include <sys/types.h>
 #include <sys/utsname.h>
 #include <grp.h>
 
-/* This header is for the sole use of libcrmcommon_test. */
+/* This header is for the sole use of libcrmcommon_test and unit tests */
 
+extern bool pcmk__mock_calloc;
 void *__real_calloc(size_t nmemb, size_t size);
 void *__wrap_calloc(size_t nmemb, size_t size);
 
+extern bool pcmk__mock_getenv;
 char *__real_getenv(const char *name);
 char *__wrap_getenv(const char *name);
 
+extern bool pcmk__mock_grent;
+void __real_setgrent(void);
+void __wrap_setgrent(void);
+struct group * __wrap_getgrent(void);
+struct group * __real_getgrent(void);
+void __wrap_endgrent(void);
+void __real_endgrent(void);
+
+extern bool pcmk__mock_getpwnam_r;
 int __real_getpwnam_r(const char *name, struct passwd *pwd,
                       char *buf, size_t buflen, struct passwd **result);
 int __wrap_getpwnam_r(const char *name, struct passwd *pwd,
                       char *buf, size_t buflen, struct passwd **result);
 
+extern bool pcmk__mock_uname;
 int __real_uname(struct utsname *buf);
 int __wrap_uname(struct utsname *buf);
 
-void __real_setgrent(void);
-void __wrap_setgrent(void);
-
-struct group *__real_getgrent(void);
-struct group *__wrap_getgrent(void);
-
-void __real_endgrent(void);
-void __wrap_endgrent(void);
-
-
 #endif  // MOCK_PRIVATE__H
diff --git a/lib/common/tests/acl/Makefile.am b/lib/common/tests/acl/Makefile.am
index a73fc354c5..93aa87a5f6 100644
--- a/lib/common/tests/acl/Makefile.am
+++ b/lib/common/tests/acl/Makefile.am
@@ -1,28 +1,26 @@
 #
 # Copyright 2021-2022 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.
 #
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include -I$(top_srcdir)/lib/common
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
 
-pcmk__is_user_in_group_test_LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la -lcmocka
-pcmk__is_user_in_group_test_LDFLAGS = \
-    -Wl,--wrap=setgrent \
-    -Wl,--wrap=getgrent \
-    -Wl,--wrap=endgrent
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 
 check_PROGRAMS = \
     pcmk__is_user_in_group_test \
 	pcmk_acl_required_test \
     xml_acl_denied_test \
     xml_acl_enabled_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/acl/pcmk__is_user_in_group_test.c b/lib/common/tests/acl/pcmk__is_user_in_group_test.c
index 67b8c2c7c1..b902f5c902 100644
--- a/lib/common/tests/acl/pcmk__is_user_in_group_test.c
+++ b/lib/common/tests/acl/pcmk__is_user_in_group_test.c
@@ -1,92 +1,50 @@
 /*
  * Copyright 2020-2022 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/acl.h>
 #include "../../crmcommon_private.h"
 
 #include "mock_private.h"
 
 #include <stdarg.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <setjmp.h>
 #include <cmocka.h>
 
-// THe index of the group that is going to be returned next from "get group entry" (getgrent)
-static int group_idx = 0;
-
-// Data used for testing
-static const char* grp0_members[] = {
-    "user0", "user1", NULL
-};
-
-static const char* grp1_members[] = {
-    "user1", NULL
-};
-
-static const char* grp2_members[] = {
-    "user2", "user1", NULL
-};
-
-// an array of "groups" (a struct from grp.h), the members of the groups are initalized here to some testing data.
-// Casting away the consts to make the compiler happy and simplify initialization. 
-// We never actually change these variables during the test!
-// string literal = const char* (cannot be changed b/c ? ) vs. char* (its getting casted to this)
-static const int NUM_GROUPS = 3;
-static struct group groups[] = {
-    {(char*)"grp0", (char*)"", 0, (char**)grp0_members},
-    {(char*)"grp1", (char*)"", 1, (char**)grp1_members},
-    {(char*)"grp2", (char*)"", 2, (char**)grp2_members},
-};
-
-// This function resets the group_idx to 0.
-void
-__wrap_setgrent(void) {
-    group_idx = 0;
-}
-
-// This function returns the next group entry in the list of groups, or
-// NULL if there aren't any left.
-// group_idx is a global variable which keeps track of where you are in the list
-struct group *
-__wrap_getgrent(void) {
-    if(group_idx >= NUM_GROUPS) return NULL;
-    return &groups[group_idx++];
-}
-
-void
-__wrap_endgrent(void) {
-}
-
 static void
 is_pcmk__is_user_in_group(void **state)
 {
+    pcmk__mock_grent = true;
+
     // null user
     assert_false(pcmk__is_user_in_group(NULL, "grp0"));
     // null group
     assert_false(pcmk__is_user_in_group("user0", NULL));
     // nonexistent group
     assert_false(pcmk__is_user_in_group("user0", "nonexistent_group"));
     // user is in group
     assert_true(pcmk__is_user_in_group("user0", "grp0"));
     // user is not in group
     assert_false(pcmk__is_user_in_group("user2", "grp0"));
+
+    pcmk__mock_grent = false;
 }
 
 int
 main(int argc, char **argv)
 {
     const struct CMUnitTest tests[] = {
         cmocka_unit_test(is_pcmk__is_user_in_group)
     };
 
     cmocka_set_message_output(CM_OUTPUT_TAP);
     return cmocka_run_group_tests(tests, NULL, NULL);
 }
diff --git a/lib/common/tests/agents/Makefile.am b/lib/common/tests/agents/Makefile.am
index e59585c117..6ef8a7f418 100644
--- a/lib/common/tests/agents/Makefile.am
+++ b/lib/common/tests/agents/Makefile.am
@@ -1,17 +1,21 @@
 #
-# Copyright 2020 the Pacemaker project contributors
+# Copyright 2020-2022 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.
 #
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS = pcmk_stonith_param_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/cmdline/Makefile.am b/lib/common/tests/cmdline/Makefile.am
index 73c81531ae..85708f952f 100644
--- a/lib/common/tests/cmdline/Makefile.am
+++ b/lib/common/tests/cmdline/Makefile.am
@@ -1,18 +1,22 @@
 #
 # Copyright 2020-2022 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.
 #
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS = pcmk__cmdline_preproc_test \
 				 pcmk__quote_cmdline_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/flags/Makefile.am b/lib/common/tests/flags/Makefile.am
index 97160bd345..9e839b71e5 100644
--- a/lib/common/tests/flags/Makefile.am
+++ b/lib/common/tests/flags/Makefile.am
@@ -1,21 +1,25 @@
 #
-# Copyright 2020-2021 the Pacemaker project contributors
+# Copyright 2020-2022 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.
 #
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS = \
 	pcmk__clear_flags_as_test	\
 	pcmk__set_flags_as_test		\
 	pcmk_all_flags_set_test		\
 	pcmk_any_flags_set_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/health/Makefile.am b/lib/common/tests/health/Makefile.am
index 4fa40c475e..84deabccce 100644
--- a/lib/common/tests/health/Makefile.am
+++ b/lib/common/tests/health/Makefile.am
@@ -1,21 +1,22 @@
 #
 # Copyright 2022 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.
 #
 
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
-
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la \
-	-lcmocka
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS = pcmk__parse_health_strategy_test	\
 		 pcmk__validate_health_strategy_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/io/Makefile.am b/lib/common/tests/io/Makefile.am
index 4f8bd9c220..8b26993c30 100644
--- a/lib/common/tests/io/Makefile.am
+++ b/lib/common/tests/io/Makefile.am
@@ -1,24 +1,24 @@
 #
-# Copyright 2020-2021 the Pacemaker project contributors
+# Copyright 2020-2022 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.
 #
 
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include -I$(top_srcdir)/lib/common
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
-
-pcmk__get_tmpdir_test_LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la -lcmocka
-pcmk__get_tmpdir_test_LDFLAGS = -Wl,--wrap=getenv
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS = \
 	pcmk__full_path_test \
 	pcmk__get_tmpdir_test
 
 TESTS = $(check_PROGRAMS)
 
diff --git a/lib/common/tests/io/pcmk__get_tmpdir_test.c b/lib/common/tests/io/pcmk__get_tmpdir_test.c
index 7fbad8f47d..803ed0185f 100644
--- a/lib/common/tests/io/pcmk__get_tmpdir_test.c
+++ b/lib/common/tests/io/pcmk__get_tmpdir_test.c
@@ -1,93 +1,76 @@
 /*
- * Copyright 2021 the Pacemaker project contributors
+ * Copyright 2021-2022 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 "mock_private.h"
 
 #include <stdarg.h>
 #include <stdbool.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
 #include <setjmp.h>
 #include <cmocka.h>
 
-static bool use_mocked = false;
-
-char *
-__wrap_getenv(const char *name)
-{
-    /* If coverage is enabled, something in "make check" will want to call
-     * the real getenv(), which will of course fail if the mocked version gets
-     * used.  This bool needs to be set and unset around every unit test so
-     * the mocked version is only called in that time.
-     */
-    if (use_mocked) {
-        return mock_ptr_type(char *);
-    } else {
-        return __real_getenv(name);
-    }
-}
-
 static void
 getenv_returns_invalid(void **state)
 {
     const char *result;
 
-    use_mocked = true;
+    pcmk__mock_getenv = true;
 
     will_return(__wrap_getenv, NULL);                   // getenv("TMPDIR") return value
     result = pcmk__get_tmpdir();
     assert_string_equal(result, "/tmp");
 
     will_return(__wrap_getenv, "");                     // getenv("TMPDIR") return value
     result = pcmk__get_tmpdir();
     assert_string_equal(result, "/tmp");
 
     will_return(__wrap_getenv, "subpath");              // getenv("TMPDIR") return value
     result = pcmk__get_tmpdir();
     assert_string_equal(result, "/tmp");
 
-    use_mocked = false;
+    pcmk__mock_getenv = false;
 }
 
 static void
 getenv_returns_valid(void **state)
 {
     const char *result;
 
-    use_mocked = true;
+    pcmk__mock_getenv = true;
 
     will_return(__wrap_getenv, "/var/tmp");             // getenv("TMPDIR") return value
     result = pcmk__get_tmpdir();
     assert_string_equal(result, "/var/tmp");
 
     will_return(__wrap_getenv, "/");                    // getenv("TMPDIR") return value
     result = pcmk__get_tmpdir();
     assert_string_equal(result, "/");
 
     will_return(__wrap_getenv, "/tmp/abcd.1234");       // getenv("TMPDIR") return value
     result = pcmk__get_tmpdir();
     assert_string_equal(result, "/tmp/abcd.1234");
 
-    use_mocked = false;
+    pcmk__mock_getenv = false;
 }
 
 int
 main(int argc, char **argv)
 {
     const struct CMUnitTest tests[] = {
         cmocka_unit_test(getenv_returns_invalid),
         cmocka_unit_test(getenv_returns_valid),
     };
 
     cmocka_set_message_output(CM_OUTPUT_TAP);
     return cmocka_run_group_tests(tests, NULL, NULL);
 }
diff --git a/lib/common/tests/iso8601/Makefile.am b/lib/common/tests/iso8601/Makefile.am
index 7bbc111282..254f413233 100644
--- a/lib/common/tests/iso8601/Makefile.am
+++ b/lib/common/tests/iso8601/Makefile.am
@@ -1,17 +1,21 @@
 #
-# Copyright 2020-2021 the Pacemaker project contributors
+# Copyright 2020-2022 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.
 #
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS = pcmk__readable_interval_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/lists/Makefile.am b/lib/common/tests/lists/Makefile.am
index 80c7018311..ce11e6833d 100644
--- a/lib/common/tests/lists/Makefile.am
+++ b/lib/common/tests/lists/Makefile.am
@@ -1,19 +1,23 @@
 #
 # Copyright 2022 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.
 #
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 
 check_PROGRAMS = \
 	pcmk__subtract_lists_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/nvpair/Makefile.am b/lib/common/tests/nvpair/Makefile.am
index 998a931387..79bbcc9074 100644
--- a/lib/common/tests/nvpair/Makefile.am
+++ b/lib/common/tests/nvpair/Makefile.am
@@ -1,19 +1,23 @@
 #
-# Copyright 2021 the Pacemaker project contributors
+# Copyright 2021-2022 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.
 #
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS =	pcmk__xe_attr_is_true_test \
 					pcmk__xe_get_bool_attr_test \
 					pcmk__xe_set_bool_attr_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/operations/Makefile.am b/lib/common/tests/operations/Makefile.am
index 457c5f7c7a..e4d6fb32b8 100644
--- a/lib/common/tests/operations/Makefile.am
+++ b/lib/common/tests/operations/Makefile.am
@@ -1,20 +1,24 @@
 #
-# Copyright 2020-2021 the Pacemaker project contributors
+# Copyright 2020-2022 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.
 #
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS = parse_op_key_test \
 				 pcmk_is_probe_test \
 				 pcmk_xe_is_probe_test \
 				 pcmk_xe_mask_probe_failure_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/results/Makefile.am b/lib/common/tests/results/Makefile.am
index d57f134d81..c0e2560331 100644
--- a/lib/common/tests/results/Makefile.am
+++ b/lib/common/tests/results/Makefile.am
@@ -1,18 +1,21 @@
 #
-# Copyright 2021 the Pacemaker project contributors
+# Copyright 2021-2022 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.
 #
 
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS =	pcmk__results_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/scores/Makefile.am b/lib/common/tests/scores/Makefile.am
index 395415ffee..2cc938cfd4 100644
--- a/lib/common/tests/scores/Makefile.am
+++ b/lib/common/tests/scores/Makefile.am
@@ -1,22 +1,25 @@
 #
 # Copyright 2020-2022 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.
 #
 
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include -I$(top_srcdir)/lib/common
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS =		\
 	char2score_test		\
 	pcmk__add_scores_test	\
 	score2char_stack_test	\
 	score2char_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/strings/Makefile.am b/lib/common/tests/strings/Makefile.am
index 73e373a69d..28680035c9 100644
--- a/lib/common/tests/strings/Makefile.am
+++ b/lib/common/tests/strings/Makefile.am
@@ -1,41 +1,44 @@
 #
 # Copyright 2020-2022 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.
 #
 
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS = \
 		crm_get_msec_test 	\
 		crm_is_true_test 	\
 		crm_str_to_boolean_test 	\
 		pcmk__add_word_test		\
 		pcmk__btoa_test			\
 		pcmk__char_in_any_str_test	\
 		pcmk__compress_test	\
 		pcmk__ends_with_test 		\
 		pcmk__guint_from_hash_test	\
 		pcmk__numeric_strcasecmp_test \
 		pcmk__parse_ll_range_test	\
 		pcmk__scan_double_test		\
 		pcmk__scan_min_int_test		\
 		pcmk__scan_port_test		\
 		pcmk__starts_with_test 		\
 		pcmk__str_any_of_test		\
 		pcmk__str_in_list_test		\
 		pcmk__str_table_dup_test	\
 		pcmk__str_update_test		\
 		pcmk__strcmp_test 			\
 		pcmk__strkey_table_test 	\
 		pcmk__strikey_table_test 	\
 		pcmk__trim_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/utils/Makefile.am b/lib/common/tests/utils/Makefile.am
index 54cea48ba0..7266b0e624 100644
--- a/lib/common/tests/utils/Makefile.am
+++ b/lib/common/tests/utils/Makefile.am
@@ -1,38 +1,32 @@
 #
 # Copyright 2020-2022 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.
 #
 
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include -I$(top_srcdir)/lib/common
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
-crm_user_lookup_test_LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la -lcmocka
-crm_user_lookup_test_LDFLAGS = -Wl,--wrap=calloc -Wl,--wrap=getpwnam_r
-
-pcmk_daemon_user_test_LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la -lcmocka
-pcmk_daemon_user_test_LDFLAGS = -Wl,--wrap=getpwnam_r
-
-pcmk_hostname_test_LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la -lcmocka
-pcmk_hostname_test_LDFLAGS = -Wl,--wrap=uname
-
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS =				\
 	compare_version_test			\
 	crm_meta_name_test			\
 	crm_meta_value_test			\
 	crm_user_lookup_test			\
 	pcmk_daemon_user_test			\
 	pcmk_str_is_infinity_test		\
 	pcmk_str_is_minus_infinity_test
 
 if WRAPPABLE_UNAME
 check_PROGRAMS += pcmk_hostname_test
 endif
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/utils/crm_user_lookup_test.c b/lib/common/tests/utils/crm_user_lookup_test.c
index 2bd8e13f4f..c2e3aaf09d 100644
--- a/lib/common/tests/utils/crm_user_lookup_test.c
+++ b/lib/common/tests/utils/crm_user_lookup_test.c
@@ -1,127 +1,110 @@
 /*
  * Copyright 2022 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 "mock_private.h"
 
 #include <pwd.h>
 #include <stdarg.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <setjmp.h>
 #include <cmocka.h>
 #include <sys/types.h>
 
-void *
-__wrap_calloc(size_t nmemb, size_t size)
-{
-    int fail = mock_type(int);
-
-    if (fail) {
-        return mock_ptr_type(void *);
-    } else {
-        return __real_calloc(nmemb, size);
-    }
-}
-
-int
-__wrap_getpwnam_r(const char *name, struct passwd *pwd, char *buf, size_t buflen,
-                  struct passwd **result)
-{
-    int retval = mock_type(int);
-
-    *result = mock_ptr_type(struct passwd *);
-
-    return retval;
-}
-
 static void
 calloc_fails(void **state)
 {
     uid_t uid;
     gid_t gid;
 
-    /* Test calloc() returning NULL. */
-
-    will_return(__wrap_calloc, 1);                      // calloc() should fail
-    will_return(__wrap_calloc, NULL);                   // calloc() return value
+    pcmk__mock_calloc = true;   // calloc() will return NULL
 
     assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -ENOMEM);
+
+    pcmk__mock_calloc = false;  // Use real calloc()
 }
 
 static void
 getpwnam_r_fails(void **state)
 {
     uid_t uid;
     gid_t gid;
 
-    will_return_always(__wrap_calloc, 0);               // calloc() should never fail
-
-    will_return(__wrap_getpwnam_r, EIO);                // getpwnam_r() return value
-    will_return(__wrap_getpwnam_r, NULL);               // result parameter to getpwnam_r()
+    // Set getpwnam_r() return value and result parameter
+    pcmk__mock_getpwnam_r = true;
+    will_return(__wrap_getpwnam_r, EIO);
+    will_return(__wrap_getpwnam_r, NULL);
 
     assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -EIO);
+
+    pcmk__mock_getpwnam_r = false;
 }
 
 static void
 no_matching_pwent(void **state)
 {
     uid_t uid;
     gid_t gid;
 
-    will_return_always(__wrap_calloc, 0);               // calloc() should never fail
-
-    will_return(__wrap_getpwnam_r, 0);                  // getpwnam_r() return value
-    will_return(__wrap_getpwnam_r, NULL);               // result parameter to getpwnam_r()
+    // Set getpwnam_r() return value and result parameter
+    pcmk__mock_getpwnam_r = true;
+    will_return(__wrap_getpwnam_r, 0);
+    will_return(__wrap_getpwnam_r, NULL);
 
     assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -EINVAL);
+
+    pcmk__mock_getpwnam_r = false;
 }
 
 static void
 entry_found(void **state)
 {
     uid_t uid;
     gid_t gid;
 
     /* We don't care about any of the other fields of the password entry, so just
      * leave them blank.
      */
     struct passwd returned_ent = { .pw_uid = 1000, .pw_gid = 1000 };
 
-    will_return_always(__wrap_calloc, 0);               // calloc() should never fail
-
     /* Test getpwnam_r returning a valid passwd entry, but we don't pass uid or gid. */
 
-    will_return(__wrap_getpwnam_r, 0);                  // getpwnam_r() return value
-    will_return(__wrap_getpwnam_r, &returned_ent);      // result parameter to getpwnam_r()
+    // Set getpwnam_r() return value and result parameter
+    pcmk__mock_getpwnam_r = true;
+    will_return(__wrap_getpwnam_r, 0);
+    will_return(__wrap_getpwnam_r, &returned_ent);
 
     assert_int_equal(crm_user_lookup("hauser", NULL, NULL), 0);
 
     /* Test getpwnam_r returning a valid passwd entry, and we do pass uid and gid. */
 
-    will_return(__wrap_getpwnam_r, 0);                  // getpwnam_r() return value
-    will_return(__wrap_getpwnam_r, &returned_ent);      // result parameter to getpwnam_r()
+    // Set getpwnam_r() return value and result parameter
+    will_return(__wrap_getpwnam_r, 0);
+    will_return(__wrap_getpwnam_r, &returned_ent);
 
     assert_int_equal(crm_user_lookup("hauser", &uid, &gid), 0);
     assert_int_equal(uid, 1000);
     assert_int_equal(gid, 1000);
+
+    pcmk__mock_getpwnam_r = false;
 }
 
 int main(int argc, char **argv)
 {
     const struct CMUnitTest tests[] = {
         cmocka_unit_test(calloc_fails),
         cmocka_unit_test(getpwnam_r_fails),
         cmocka_unit_test(no_matching_pwent),
         cmocka_unit_test(entry_found),
     };
 
     cmocka_set_message_output(CM_OUTPUT_TAP);
     return cmocka_run_group_tests(tests, NULL, NULL);
 }
diff --git a/lib/common/tests/utils/pcmk_daemon_user_test.c b/lib/common/tests/utils/pcmk_daemon_user_test.c
index b58bf15f42..c911166d77 100644
--- a/lib/common/tests/utils/pcmk_daemon_user_test.c
+++ b/lib/common/tests/utils/pcmk_daemon_user_test.c
@@ -1,73 +1,79 @@
 /*
  * Copyright 2022 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 "mock_private.h"
 
 #include <pwd.h>
 #include <stdarg.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <setjmp.h>
 #include <cmocka.h>
 #include <sys/types.h>
 
-int
-__wrap_getpwnam_r(const char *name, struct passwd *pwd, char *buf, size_t buflen,
-                  struct passwd **result)
-{
-    *result = mock_ptr_type(struct passwd *);
-    return 0;
-}
-
 static void
 no_matching_pwent(void **state)
 {
     uid_t uid;
     gid_t gid;
 
-    will_return(__wrap_getpwnam_r, NULL);               // result parameter to getpwnam_r()
+    // Set getpwnam_r() return value and result parameter
+    pcmk__mock_getpwnam_r = true;
+    will_return(__wrap_getpwnam_r, ENOENT);
+    will_return(__wrap_getpwnam_r, NULL);
 
-    assert_int_equal(pcmk_daemon_user(&uid, &gid), -EINVAL);
+    assert_int_equal(pcmk_daemon_user(&uid, &gid), -ENOENT);
+
+    pcmk__mock_getpwnam_r = false;
 }
 
 static void
 entry_found(void **state)
 {
     uid_t uid;
     gid_t gid;
 
     /* We don't care about any of the other fields of the password entry, so just
      * leave them blank.
      */
     struct passwd returned_ent = { .pw_uid = 1000, .pw_gid = 1000 };
 
     /* Test getpwnam_r returning a valid passwd entry, but we don't pass uid or gid. */
 
-    will_return_always(__wrap_getpwnam_r, &returned_ent);      // result parameter to getpwnam_r()
+    // Set getpwnam_r() return value and result parameter
+    pcmk__mock_getpwnam_r = true;
+    will_return(__wrap_getpwnam_r, 0);
+    will_return(__wrap_getpwnam_r, &returned_ent);
 
     assert_int_equal(pcmk_daemon_user(NULL, NULL), 0);
 
     /* Test getpwnam_r returning a valid passwd entry, and we do pass uid and gid. */
 
+    /* We don't need to call will_return() again because pcmk_daemon_user()
+     * will have cached the uid/gid from the previous call and won't make
+     * another call to getpwnam_r().
+     */
     assert_int_equal(pcmk_daemon_user(&uid, &gid), 0);
     assert_int_equal(uid, 1000);
     assert_int_equal(gid, 1000);
+
+    pcmk__mock_getpwnam_r = false;
 }
 
 int main(int argc, char **argv)
 {
     const struct CMUnitTest tests[] = {
         cmocka_unit_test(no_matching_pwent),
         cmocka_unit_test(entry_found),
     };
 
     cmocka_set_message_output(CM_OUTPUT_TAP);
     return cmocka_run_group_tests(tests, NULL, NULL);
 }
diff --git a/lib/common/tests/utils/pcmk_hostname_test.c b/lib/common/tests/utils/pcmk_hostname_test.c
index 2af27da2aa..c6f156c940 100644
--- a/lib/common/tests/utils/pcmk_hostname_test.c
+++ b/lib/common/tests/utils/pcmk_hostname_test.c
@@ -1,71 +1,65 @@
 /*
  * Copyright 2021 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 "mock_private.h"
 
 #include <stdarg.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
 #include <setjmp.h>
 #include <cmocka.h>
 
 #include <sys/utsname.h>
 
-int
-__wrap_uname(struct utsname *buf)
-{
-    int retval = mock_type(int);
-
-    if (retval == 0) {
-        strcpy(buf->nodename, mock_ptr_type(char *));
-    }
-
-    return retval;
-}
-
 static void
 uname_succeeded_test(void **state)
 {
     char *retval;
 
-    will_return(__wrap_uname, 0);                       // uname() return value
-    will_return(__wrap_uname, "somename");              // uname() buf->nodename
+    // Set uname() return value and buf parameter node name
+    pcmk__mock_uname = true;
+    will_return(__wrap_uname, 0);
+    will_return(__wrap_uname, "somename");
 
     retval = pcmk_hostname();
     assert_non_null(retval);
     assert_string_equal("somename", retval);
 
     free(retval);
+
+    pcmk__mock_uname = false;
 }
 
 static void
 uname_failed_test(void **state)
 {
-    char *retval;
+    // Set uname() return value and buf parameter node name
+    pcmk__mock_uname = true;
+    will_return(__wrap_uname, -1);
+    will_return(__wrap_uname, NULL);
 
-    will_return(__wrap_uname, -1);                      // uname() return value
+    assert_null(pcmk_hostname());
 
-    retval = pcmk_hostname();
-    assert_null(retval);
+    pcmk__mock_uname = false;
 }
 
 int
 main(int argc, char **argv)
 {
     const struct CMUnitTest tests[] = {
         cmocka_unit_test(uname_succeeded_test),
         cmocka_unit_test(uname_failed_test),
     };
 
     cmocka_set_message_output(CM_OUTPUT_TAP);
     return cmocka_run_group_tests(tests, NULL, NULL);
 }
diff --git a/lib/common/tests/xpath/Makefile.am b/lib/common/tests/xpath/Makefile.am
index a4cba33986..3d669132d2 100644
--- a/lib/common/tests/xpath/Makefile.am
+++ b/lib/common/tests/xpath/Makefile.am
@@ -1,17 +1,21 @@
 #
-# Copyright 2021 the Pacemaker project contributors
+# Copyright 2021-2022 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.
 #
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
-LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
+
+AM_CPPFLAGS = -I$(top_srcdir)/include		\
+	      -I$(top_builddir)/include		\
+	      -I$(top_srcdir)/lib/common
+LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la
+AM_LDFLAGS = $(LDFLAGS_WRAP)
 
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 check_PROGRAMS =	pcmk__xpath_node_id_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/mk/tap.mk b/mk/tap.mk
index 3c531d549e..e5dbd39812 100644
--- a/mk/tap.mk
+++ b/mk/tap.mk
@@ -1,16 +1,25 @@
 #
-# Copyright 2021 the Pacemaker project contributors
+# Copyright 2021-2022 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.
 #
 
 AM_TESTS_ENVIRONMENT= \
 	G_DEBUG=gc-friendly 			\
 	MALLOC_CHECK_=2 			\
 	MALLOC_PERTURB_=$$(($${RANDOM:-256} % 256))
 LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tests/tap-driver.sh
 LOG_COMPILER = $(top_srcdir)/tests/tap-test
 CLEANFILES = *.log *.trs
+
+WRAPPED = calloc		\
+	  endgrent		\
+	  getenv		\
+	  getgrent		\
+	  getpwnam_r		\
+	  setgrent		\
+	  uname
+LDFLAGS_WRAP = $(foreach fn,$(WRAPPED),-Wl,--wrap=$(fn))