diff --git a/include/crm/common/internal.h b/include/crm/common/internal.h index ceb831c41d..d19b8f55d2 100644 --- a/include/crm/common/internal.h +++ b/include/crm/common/internal.h @@ -1,348 +1,348 @@ /* * Copyright 2015-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #ifndef PCMK__CRM_COMMON_INTERNAL__H #define PCMK__CRM_COMMON_INTERNAL__H #include // pid_t, getpid() #include // bool #include // uint8_t, uint64_t #include // PRIu64 #include // guint, GList, GHashTable #include // xmlNode #include // do_crm_log_unlikely(), etc. #include // mainloop_io_t, struct ipc_client_callbacks #include // crm_strdup_printf() #include #include #include #include #include #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif /* This says whether the current application is a Pacemaker daemon or not, * and is used to change default logging settings such as whether to log to * stderr, etc., as well as a few other details such as whether blackbox signal * handling is enabled. * * It is set when logging is initialized, and does not need to be set directly. */ extern bool pcmk__is_daemon; // Number of elements in a statically defined array #define PCMK__NELEM(a) ((int) (sizeof(a)/sizeof(a[0])) ) #if PCMK__ENABLE_CIBSECRETS /* internal CIB utilities (from cib_secrets.c) */ int pcmk__substitute_secrets(const char *rsc_id, GHashTable *params); #endif /* internal main loop utilities (from mainloop.c) */ int pcmk__add_mainloop_ipc(crm_ipc_t *ipc, int priority, void *userdata, const struct ipc_client_callbacks *callbacks, mainloop_io_t **source); guint pcmk__mainloop_timer_get_period(const mainloop_timer_t *timer); /* internal name/value utilities (from nvpair.c) */ int pcmk__scan_nvpair(const gchar *input, gchar **name, gchar **value); char *pcmk__format_nvpair(const char *name, const char *value, const char *units); /* internal procfs utilities (from procfs.c) */ pid_t pcmk__procfs_pid_of(const char *name); unsigned int pcmk__procfs_num_cores(void); -int pcmk__procfs_pid2path(pid_t pid, char path[], size_t path_size); +int pcmk__procfs_pid2path(pid_t pid, char **path); bool pcmk__procfs_has_pids(void); DIR *pcmk__procfs_fd_dir(void); void pcmk__sysrq_trigger(char t); bool pcmk__throttle_cib_load(const char *server, float *load); bool pcmk__throttle_load_avg(float *load); /* internal functions related to process IDs (from pid.c) */ /*! * \internal * \brief Check whether process exists (by PID and optionally executable path) * * \param[in] pid PID of process to check * \param[in] daemon If not NULL, path component to match with procfs entry * * \return Standard Pacemaker return code * \note Particular return codes of interest include pcmk_rc_ok for alive, * ESRCH for process is not alive (verified by kill and/or executable path * match), EACCES for caller unable or not allowed to check. A result of * "alive" is less reliable when \p daemon is not provided or procfs is * not available, since there is no guarantee that the PID has not been * recycled for another process. * \note This function cannot be used to verify \e authenticity of the process. */ int pcmk__pid_active(pid_t pid, const char *daemon); int pcmk__read_pidfile(const char *filename, pid_t *pid); int pcmk__pidfile_matches(const char *filename, pid_t expected_pid, const char *expected_name, pid_t *pid); int pcmk__lock_pidfile(const char *filename, const char *name); // bitwise arithmetic utilities /*! * \internal * \brief Set specified flags in a flag group * * \param[in] function Function name of caller * \param[in] line Line number of caller * \param[in] log_level Log a message at this level * \param[in] flag_type Label describing this flag group (for logging) * \param[in] target Name of object whose flags these are (for logging) * \param[in] flag_group Flag group being manipulated * \param[in] flags Which flags in the group should be set * \param[in] flags_str Readable equivalent of \p flags (for logging) * * \return Possibly modified flag group */ static inline uint64_t pcmk__set_flags_as(const char *function, int line, uint8_t log_level, const char *flag_type, const char *target, uint64_t flag_group, uint64_t flags, const char *flags_str) { uint64_t result = flag_group | flags; if (result != flag_group) { do_crm_log_unlikely(log_level, "%s flags %#.8" PRIx64 " (%s) for %s set by %s:%d", pcmk__s(flag_type, "Group of"), flags, pcmk__s(flags_str, "flags"), pcmk__s(target, "target"), function, line); } return result; } /*! * \internal * \brief Clear specified flags in a flag group * * \param[in] function Function name of caller * \param[in] line Line number of caller * \param[in] log_level Log a message at this level * \param[in] flag_type Label describing this flag group (for logging) * \param[in] target Name of object whose flags these are (for logging) * \param[in] flag_group Flag group being manipulated * \param[in] flags Which flags in the group should be cleared * \param[in] flags_str Readable equivalent of \p flags (for logging) * * \return Possibly modified flag group */ static inline uint64_t pcmk__clear_flags_as(const char *function, int line, uint8_t log_level, const char *flag_type, const char *target, uint64_t flag_group, uint64_t flags, const char *flags_str) { uint64_t result = flag_group & ~flags; if (result != flag_group) { do_crm_log_unlikely(log_level, "%s flags %#.8" PRIx64 " (%s) for %s cleared by %s:%d", pcmk__s(flag_type, "Group of"), flags, pcmk__s(flags_str, "flags"), pcmk__s(target, "target"), function, line); } return result; } /*! * \internal * \brief Get readable string for whether specified flags are set * * \param[in] flag_group Group of flags to check * \param[in] flags Which flags in \p flag_group should be checked * * \return "true" if all \p flags are set in \p flag_group, otherwise "false" */ static inline const char * pcmk__flag_text(uint64_t flag_group, uint64_t flags) { return pcmk__btoa(pcmk_all_flags_set(flag_group, flags)); } // miscellaneous utilities (from utils.c) void pcmk__daemonize(const char *name, const char *pidfile); void pcmk__panic(const char *reason); pid_t pcmk__locate_sbd(void); void pcmk__sleep_ms(unsigned int ms); guint pcmk__create_timer(guint interval_ms, GSourceFunc fn, gpointer data); guint pcmk__timeout_ms2s(guint timeout_ms); extern int pcmk__score_red; extern int pcmk__score_green; extern int pcmk__score_yellow; /*! * \internal * \brief Allocate new zero-initialized memory, asserting on failure * * \param[in] file File where \p function is located * \param[in] function Calling function * \param[in] line Line within \p file * \param[in] nmemb Number of elements to allocate memory for * \param[in] size Size of each element * * \return Newly allocated memory of of size nmemb * size (guaranteed * not to be \c NULL) * * \note The caller is responsible for freeing the return value using \c free(). */ static inline void * pcmk__assert_alloc_as(const char *file, const char *function, uint32_t line, size_t nmemb, size_t size) { void *ptr = calloc(nmemb, size); if (ptr == NULL) { crm_abort(file, function, line, "Out of memory", FALSE, TRUE); crm_exit(CRM_EX_OSERR); } return ptr; } /*! * \internal * \brief Allocate new zero-initialized memory, asserting on failure * * \param[in] nmemb Number of elements to allocate memory for * \param[in] size Size of each element * * \return Newly allocated memory of of size nmemb * size (guaranteed * not to be \c NULL) * * \note The caller is responsible for freeing the return value using \c free(). */ #define pcmk__assert_alloc(nmemb, size) \ pcmk__assert_alloc_as(__FILE__, __func__, __LINE__, nmemb, size) /*! * \internal * \brief Resize a dynamically allocated memory block * * \param[in] ptr Memory block to resize (or NULL to allocate new memory) * \param[in] size New size of memory block in bytes (must be > 0) * * \return Pointer to resized memory block * * \note This asserts on error, so the result is guaranteed to be non-NULL * (which is the main advantage of this over directly using realloc()). */ static inline void * pcmk__realloc(void *ptr, size_t size) { void *new_ptr; // realloc(p, 0) can replace free(p) but this wrapper can't pcmk__assert(size > 0); new_ptr = realloc(ptr, size); if (new_ptr == NULL) { free(ptr); abort(); } return new_ptr; } static inline char * pcmk__getpid_s(void) { return crm_strdup_printf("%lu", (unsigned long) getpid()); } // More efficient than g_list_length(list) == 1 static inline bool pcmk__list_of_1(GList *list) { return list && (list->next == NULL); } // More efficient than g_list_length(list) > 1 static inline bool pcmk__list_of_multiple(GList *list) { return list && (list->next != NULL); } /* convenience functions for failure-related node attributes */ #define PCMK__FAIL_COUNT_PREFIX "fail-count" #define PCMK__LAST_FAILURE_PREFIX "last-failure" /*! * \internal * \brief Generate a failure-related node attribute name for a resource * * \param[in] prefix Start of attribute name * \param[in] rsc_id Resource name * \param[in] op Operation name * \param[in] interval_ms Operation interval * * \return Newly allocated string with attribute name * * \note Failure attributes are named like PREFIX-RSC#OP_INTERVAL (for example, * "fail-count-myrsc#monitor_30000"). The '#' is used because it is not * a valid character in a resource ID, to reliably distinguish where the * operation name begins. The '_' is used simply to be more comparable to * action labels like "myrsc_monitor_30000". */ static inline char * pcmk__fail_attr_name(const char *prefix, const char *rsc_id, const char *op, guint interval_ms) { CRM_CHECK(prefix && rsc_id && op, return NULL); return crm_strdup_printf("%s-%s#%s_%u", prefix, rsc_id, op, interval_ms); } static inline char * pcmk__failcount_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__FAIL_COUNT_PREFIX, rsc_id, op, interval_ms); } static inline char * pcmk__lastfailure_name(const char *rsc_id, const char *op, guint interval_ms) { return pcmk__fail_attr_name(PCMK__LAST_FAILURE_PREFIX, rsc_id, op, interval_ms); } // internal resource agent functions (from agents.c) int pcmk__effective_rc(int rc); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_INTERNAL__H diff --git a/lib/common/mock.c b/lib/common/mock.c index bc82d7836c..2fb231ff3e 100644 --- a/lib/common/mock.c +++ b/lib/common/mock.c @@ -1,461 +1,459 @@ /* * Copyright 2021-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include // pid_t, size_t #include #include #include #include #include #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. * * HOW TO ADD A MOCKED FUNCTION: * * - 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). * * - In mock_private.h, add declarations for extern bool pcmk__mock_X and the * __real_X and __wrap_X function prototypes. * * - In mk/tap.mk, add the function name to the WRAPPED variable. * * HOW TO USE A MOCKED FUNCTION: * * - #include "mock_private.h" in your test file. * * - 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. */ // LCOV_EXCL_START /* abort() * * Always mock abort - there's no pcmk__mock_abort tuneable to control this. * Because abort calls _exit(), which doesn't run any of the things registered * with atexit(), coverage numbers do not get written out. This most noticably * affects places where we are testing that things abort when they should. * * The solution is this wrapper that is always enabled when we are running * unit tests (mock.c does not get included for the regular libcrmcommon.so). * All it does is dump coverage data and call the real abort(). */ _Noreturn void __wrap_abort(void) { #if (PCMK__WITH_COVERAGE == 1) __gcov_dump(); #endif __real_abort(); } /* calloc() * * If pcmk__mock_calloc is set to true, later calls to calloc() will return * NULL and must be preceded by: * * expect_*(__wrap_calloc, nmemb[, ...]); * expect_*(__wrap_calloc, size[, ...]); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_calloc = false; void * __wrap_calloc(size_t nmemb, size_t size) { if (!pcmk__mock_calloc) { return __real_calloc(nmemb, size); } check_expected(nmemb); check_expected(size); return NULL; } /* getenv() * * If pcmk__mock_getenv is set to true, later calls to getenv() must be preceded * by: * * expect_*(__wrap_getenv, name[, ...]); * will_return(__wrap_getenv, return_value); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_getenv = false; char * __wrap_getenv(const char *name) { if (!pcmk__mock_getenv) { return __real_getenv(name); } check_expected_ptr(name); return mock_ptr_type(char *); } /* realloc() * * If pcmk__mock_realloc is set to true, later calls to realloc() will return * NULL and must be preceded by: * * expect_*(__wrap_realloc, ptr[, ...]); * expect_*(__wrap_realloc, size[, ...]); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_realloc = false; void * __wrap_realloc(void *ptr, size_t size) { if (!pcmk__mock_realloc) { return __real_realloc(ptr, size); } check_expected_ptr(ptr); check_expected(size); return NULL; } /* setenv() * * If pcmk__mock_setenv is set to true, later calls to setenv() must be preceded * by: * * expect_*(__wrap_setenv, name[, ...]); * expect_*(__wrap_setenv, value[, ...]); * expect_*(__wrap_setenv, overwrite[, ...]); * will_return(__wrap_setenv, errno_to_set); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. */ bool pcmk__mock_setenv = false; int __wrap_setenv(const char *name, const char *value, int overwrite) { if (!pcmk__mock_setenv) { return __real_setenv(name, value, overwrite); } check_expected_ptr(name); check_expected_ptr(value); check_expected(overwrite); errno = mock_type(int); return (errno == 0)? 0 : -1; } /* unsetenv() * * If pcmk__mock_unsetenv is set to true, later calls to unsetenv() must be * preceded by: * * expect_*(__wrap_unsetenv, name[, ...]); * will_return(__wrap_setenv, errno_to_set); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. */ bool pcmk__mock_unsetenv = false; int __wrap_unsetenv(const char *name) { if (!pcmk__mock_unsetenv) { return __real_unsetenv(name); } check_expected_ptr(name); errno = mock_type(int); return (errno == 0)? 0 : -1; } /* getpid() * * If pcmk__mock_getpid is set to true, later calls to getpid() must be preceded * by: * * will_return(__wrap_getpid, return_value); */ bool pcmk__mock_getpid = false; pid_t __wrap_getpid(void) { return pcmk__mock_getpid? mock_type(pid_t) : __real_getpid(); } /* 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) { if (pcmk__mock_grent) { group_idx = 0; } else { __real_setgrent(); } } /* 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 (pcmk__mock_grent) { if (group_idx >= NUM_GROUPS) { return NULL; } return &groups[group_idx++]; } else { return __real_getgrent(); } } void __wrap_endgrent(void) { if (!pcmk__mock_grent) { __real_endgrent(); } } /* fopen() * * If pcmk__mock_fopen is set to true, later calls to fopen() must be * preceded by: * * expect_*(__wrap_fopen, pathname[, ...]); * expect_*(__wrap_fopen, mode[, ...]); * will_return(__wrap_fopen, errno_to_set); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * This has two mocked functions, since fopen() is sometimes actually fopen64(). */ bool pcmk__mock_fopen = false; FILE * __wrap_fopen(const char *pathname, const char *mode) { if (pcmk__mock_fopen) { check_expected_ptr(pathname); check_expected_ptr(mode); errno = mock_type(int); if (errno != 0) { return NULL; } else { return __real_fopen(pathname, mode); } } else { return __real_fopen(pathname, mode); } } #ifdef HAVE_FOPEN64 FILE * __wrap_fopen64(const char *pathname, const char *mode) { if (pcmk__mock_fopen) { check_expected_ptr(pathname); check_expected_ptr(mode); errno = mock_type(int); if (errno != 0) { return NULL; } else { return __real_fopen64(pathname, mode); } } else { return __real_fopen64(pathname, mode); } } #endif /* getpwnam_r() * * If pcmk__mock_getpwnam_r is set to true, later calls to getpwnam_r() must be * preceded by: * * expect_*(__wrap_getpwnam_r, name[, ...]); * expect_*(__wrap_getpwnam_r, pwd[, ...]); * expect_*(__wrap_getpwnam_r, buf[, ...]); * expect_*(__wrap_getpwnam_r, buflen[, ...]); * expect_*(__wrap_getpwnam_r, result[, ...]); * will_return(__wrap_getpwnam_r, return_value); * will_return(__wrap_getpwnam_r, ptr_to_result_struct); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ 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); check_expected_ptr(name); check_expected_ptr(pwd); check_expected_ptr(buf); check_expected(buflen); check_expected_ptr(result); *result = mock_ptr_type(struct passwd *); return retval; } else { return __real_getpwnam_r(name, pwd, buf, buflen, result); } } /* * If pcmk__mock_readlink is set to true, later calls to readlink() must be * preceded by: * * expect_*(__wrap_readlink, path[, ...]); - * expect_*(__wrap_readlink, buf[, ...]); * expect_*(__wrap_readlink, bufsize[, ...]); * will_return(__wrap_readlink, errno_to_set); * will_return(__wrap_readlink, link_contents); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html * * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. */ bool pcmk__mock_readlink = false; ssize_t __wrap_readlink(const char *restrict path, char *restrict buf, size_t bufsize) { if (pcmk__mock_readlink) { const char *contents = NULL; check_expected_ptr(path); - check_expected(buf); check_expected(bufsize); errno = mock_type(int); contents = mock_ptr_type(const char *); if (errno == 0) { strncpy(buf, contents, bufsize - 1); return strlen(contents); } return -1; } else { return __real_readlink(path, buf, bufsize); } } /* strdup() * * If pcmk__mock_strdup is set to true, later calls to strdup() will return * NULL and must be preceded by: * * expect_*(__wrap_strdup, s[, ...]); * * expect_* functions: https://api.cmocka.org/group__cmocka__param.html */ bool pcmk__mock_strdup = false; char * __wrap_strdup(const char *s) { if (!pcmk__mock_strdup) { return __real_strdup(s); } check_expected_ptr(s); return NULL; } // LCOV_EXCL_STOP diff --git a/lib/common/pid.c b/lib/common/pid.c index 5082c70a50..10235bda7e 100644 --- a/lib/common/pid.c +++ b/lib/common/pid.c @@ -1,245 +1,247 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include int pcmk__pid_active(pid_t pid, const char *daemon) { static pid_t last_asked_pid = 0; /* log spam prevention */ int rc = 0; if (pid <= 0) { return EINVAL; } rc = kill(pid, 0); if ((rc < 0) && (errno == ESRCH)) { return ESRCH; /* no such PID detected */ } else if ((daemon == NULL) || !pcmk__procfs_has_pids()) { // The kill result is all we have, we can't check the name if (rc == 0) { return pcmk_rc_ok; } rc = errno; if (last_asked_pid != pid) { crm_info("Cannot examine PID %lld: %s", (long long) pid, pcmk_rc_str(rc)); last_asked_pid = pid; } return rc; /* errno != ESRCH */ } else { /* make sure PID hasn't been reused by another process XXX: might still be just a zombie, which could confuse decisions */ bool checked_through_kill = (rc == 0); - char exe_path[PATH_MAX] = { '\0', }; + bool paths_equal = false; + char *exe_path = NULL; char *myexe_path = NULL; - rc = pcmk__procfs_pid2path(pid, exe_path, sizeof(exe_path)); + rc = pcmk__procfs_pid2path(pid, &exe_path); if (rc != pcmk_rc_ok) { if (rc != EACCES) { // Check again to filter out races if ((kill(pid, 0) < 0) && (errno == ESRCH)) { return ESRCH; } } if (last_asked_pid != pid) { if (rc == EACCES) { crm_info("Could not get executable for PID %lld: %s " QB_XS " rc=%d", (long long) pid, pcmk_rc_str(rc), rc); } else { crm_err("Could not get executable for PID %lld: %s " QB_XS " rc=%d", (long long) pid, pcmk_rc_str(rc), rc); } last_asked_pid = pid; } if (rc == EACCES) { // Trust kill if it was OK (we can't double-check via path) return checked_through_kill? pcmk_rc_ok : EACCES; } else { return ESRCH; /* most likely errno == ENOENT */ } } if (daemon[0] != '/') { myexe_path = crm_strdup_printf(CRM_DAEMON_DIR "/%s", daemon); } else { myexe_path = pcmk__str_copy(daemon); } - rc = strcmp(exe_path, myexe_path); + paths_equal = pcmk__str_eq(exe_path, myexe_path, pcmk__str_none); + free(exe_path); free(myexe_path); - if (rc == 0) { + if (paths_equal) { return pcmk_rc_ok; } } return ESRCH; } #define LOCKSTRLEN 11 /*! * \internal * \brief Read a process ID from a file * * \param[in] filename Process ID file to read * \param[out] pid Where to put PID that was read * * \return Standard Pacemaker return code */ int pcmk__read_pidfile(const char *filename, pid_t *pid) { int fd; struct stat sbuf; int rc = pcmk_rc_ok; long long pid_read = 0; char buf[LOCKSTRLEN + 1]; CRM_CHECK((filename != NULL) && (pid != NULL), return EINVAL); fd = open(filename, O_RDONLY); if (fd < 0) { return errno; } if ((fstat(fd, &sbuf) >= 0) && (sbuf.st_size < LOCKSTRLEN)) { sleep(2); /* if someone was about to create one, * give'm a sec to do so */ } if (read(fd, buf, sizeof(buf)) < 1) { rc = errno; goto bail; } errno = 0; rc = sscanf(buf, "%lld", &pid_read); if (rc > 0) { if (pid_read <= 0) { rc = ESRCH; } else { rc = pcmk_rc_ok; *pid = (pid_t) pid_read; crm_trace("Read pid %lld from %s", pid_read, filename); } } else if (rc == 0) { rc = ENODATA; } else { rc = errno; } bail: close(fd); return rc; } /*! * \internal * \brief Check whether a process from a PID file matches expected values * * \param[in] filename Path of PID file * \param[in] expected_pid If positive, compare to this PID * \param[in] expected_name If not NULL, the PID from the PID file is valid * only if it is active as a process with this name * \param[out] pid If not NULL, store PID found in PID file here * * \return Standard Pacemaker return code */ int pcmk__pidfile_matches(const char *filename, pid_t expected_pid, const char *expected_name, pid_t *pid) { pid_t pidfile_pid = 0; int rc = pcmk__read_pidfile(filename, &pidfile_pid); if (pid) { *pid = pidfile_pid; } if (rc != pcmk_rc_ok) { // Error reading PID file or invalid contents unlink(filename); rc = ENOENT; } else if ((expected_pid > 0) && (pidfile_pid == expected_pid)) { // PID in file matches what was expected rc = pcmk_rc_ok; } else if (pcmk__pid_active(pidfile_pid, expected_name) == ESRCH) { // Contains a stale value unlink(filename); rc = ENOENT; } else if ((expected_pid > 0) && (pidfile_pid != expected_pid)) { // Locked by existing process rc = EEXIST; } return rc; } /*! * \internal * \brief Create a PID file for the current process (if not already existent) * * \param[in] filename Name of PID file to create * \param[in] name Name of current process * * \return Standard Pacemaker return code */ int pcmk__lock_pidfile(const char *filename, const char *name) { pid_t mypid = getpid(); int fd = 0; int rc = 0; char buf[LOCKSTRLEN + 2]; rc = pcmk__pidfile_matches(filename, 0, name, NULL); if ((rc != pcmk_rc_ok) && (rc != ENOENT)) { // Locked by existing process return rc; } fd = open(filename, O_CREAT | O_WRONLY | O_EXCL, 0644); if (fd < 0) { return errno; } snprintf(buf, sizeof(buf), "%*lld\n", LOCKSTRLEN - 1, (long long) mypid); rc = write(fd, buf, LOCKSTRLEN); close(fd); if (rc != LOCKSTRLEN) { crm_perror(LOG_ERR, "Incomplete write to %s", filename); return errno; } rc = pcmk__pidfile_matches(filename, mypid, name, NULL); if (rc != pcmk_rc_ok) { // Something is really wrong -- maybe I/O error on read back? unlink(filename); } return rc; } diff --git a/lib/common/procfs.c b/lib/common/procfs.c index a6b0d8e7ac..ae02638cb2 100644 --- a/lib/common/procfs.c +++ b/lib/common/procfs.c @@ -1,465 +1,477 @@ /* - * Copyright 2015-2024 the Pacemaker project contributors + * Copyright 2015-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #if HAVE_LINUX_PROCFS /*! * \internal * \brief Return name of /proc file containing the CIB daemon's load statistics * * \return Newly allocated memory with file name on success, NULL otherwise * * \note It is the caller's responsibility to free the return value. * This will return NULL if the daemon is being run via valgrind. * This should be called only on Linux systems. */ static char * find_cib_loadfile(const char *server) { pid_t pid = pcmk__procfs_pid_of(server); return pid? crm_strdup_printf("/proc/%lld/stat", (long long) pid) : NULL; } /*! * \internal * \brief Get process ID and name associated with a /proc directory entry * * \param[in] entry Directory entry (must be result of readdir() on /proc) * \param[out] name If not NULL, a char[16] to hold the process name * \param[out] pid If not NULL, will be set to process ID of entry * * \return Standard Pacemaker return code * \note This should be called only on Linux systems, as not all systems that * support /proc store process names and IDs in the same way. The kernel * limits the process name to the first 15 characters (plus terminator). * It would be nice if there were a public kernel API constant for that * limit, but there isn't. */ static int pcmk__procfs_process_info(const struct dirent *entry, char *name, pid_t *pid) { int fd, local_pid; FILE *file; struct stat statbuf; char procpath[128] = { 0 }; /* We're only interested in entries whose name is a PID, * so skip anything non-numeric or that is too long. * * 114 = 128 - strlen("/proc/") - strlen("/status") - 1 */ local_pid = atoi(entry->d_name); if ((local_pid <= 0) || (strlen(entry->d_name) > 114)) { return -1; } if (pid) { *pid = (pid_t) local_pid; } /* Get this entry's file information */ strcpy(procpath, "/proc/"); strcat(procpath, entry->d_name); fd = open(procpath, O_RDONLY); if (fd < 0 ) { return -1; } if (fstat(fd, &statbuf) < 0) { close(fd); return -1; } close(fd); /* We're only interested in subdirectories */ if (!S_ISDIR(statbuf.st_mode)) { return -1; } /* Read the first entry ("Name:") from the process's status file. * We could handle the valgrind case if we parsed the cmdline file * instead, but that's more of a pain than it's worth. */ if (name != NULL) { strcat(procpath, "/status"); file = fopen(procpath, "r"); if (!file) { return -1; } if (fscanf(file, "Name:\t%15[^\n]", name) != 1) { fclose(file); return -1; } name[15] = 0; fclose(file); } return 0; } #endif // HAVE_LINUX_PROCFS /*! * \internal * \brief Return process ID of a named process * * \param[in] name Process name (as used in /proc/.../status) * * \return Process ID of named process if running, 0 otherwise * * \note This will return 0 if the process is being run via valgrind. * This should be called only on Linux systems. */ pid_t pcmk__procfs_pid_of(const char *name) { #if HAVE_LINUX_PROCFS DIR *dp; struct dirent *entry; pid_t pid = 0; char entry_name[64] = { 0 }; dp = opendir("/proc"); if (dp == NULL) { crm_notice("Can not read /proc directory to track existing components"); return 0; } while ((entry = readdir(dp)) != NULL) { if ((pcmk__procfs_process_info(entry, entry_name, &pid) == pcmk_rc_ok) && pcmk__str_eq(entry_name, name, pcmk__str_casei) && (pcmk__pid_active(pid, NULL) == pcmk_rc_ok)) { crm_info("Found %s active as process %lld", name, (long long) pid); break; } pid = 0; } closedir(dp); return pid; #else return 0; #endif // HAVE_LINUX_PROCFS } /*! * \internal * \brief Calculate number of logical CPU cores from procfs * * \return Number of cores (or 1 if unable to determine) */ unsigned int pcmk__procfs_num_cores(void) { #if HAVE_LINUX_PROCFS int cores = 0; FILE *stream = NULL; /* Parse /proc/stat instead of /proc/cpuinfo because it's smaller */ stream = fopen("/proc/stat", "r"); if (stream == NULL) { crm_perror(LOG_INFO, "Could not open /proc/stat"); } else { char buffer[2048]; while (fgets(buffer, sizeof(buffer), stream)) { if (pcmk__starts_with(buffer, "cpu") && isdigit(buffer[3])) { ++cores; } } fclose(stream); } return cores? cores : 1; #else return 1; #endif // HAVE_LINUX_PROCFS } /*! * \internal * \brief Get the executable path corresponding to a process ID * - * \param[in] pid Process ID to check - * \param[out] path Where to store executable path - * \param[in] path_size Size of \p path in characters (ideally PATH_MAX) + * \param[in] pid Process ID to check + * \param[out] path Where to store executable path (can be \c NULL) * * \return Standard Pacemaker error code (as possible errno values from * readlink()) */ int -pcmk__procfs_pid2path(pid_t pid, char path[], size_t path_size) +pcmk__procfs_pid2path(pid_t pid, char **path) { #if HAVE_LINUX_PROCFS char procfs_exe_path[PATH_MAX]; ssize_t link_rc; + /* The readlink(2) man page recommends calling lstat() to get the required + * buffer size, and then dynamically allocate the buffer. However, + * st_size == 0 for symlinks under /proc. So we use PATH_MAX. + */ + char real_path[PATH_MAX] = { '\0', }; + + pcmk__assert((path == NULL) || (*path == NULL)); + if (snprintf(procfs_exe_path, PATH_MAX, "/proc/%lld/exe", (long long) pid) >= PATH_MAX) { return ENAMETOOLONG; // Truncated (shouldn't be possible in practice) } - link_rc = readlink(procfs_exe_path, path, path_size - 1); + link_rc = readlink(procfs_exe_path, real_path, sizeof(real_path)); if (link_rc < 0) { return errno; - } else if (link_rc >= (path_size - 1)) { + } else if (link_rc >= sizeof(real_path)) { return ENAMETOOLONG; } - path[link_rc] = '\0'; + if (path != NULL) { + /* Make Coverity happy; we already zero-initialized real_path, and we + * returned ENAMETOOLONG if it's no longer null-terminated + */ + real_path[link_rc] = '\0'; + + *path = pcmk__str_copy(real_path); + } return pcmk_rc_ok; #else return EOPNOTSUPP; #endif // HAVE_LINUX_PROCFS } /*! * \internal * \brief Check whether process ID information is available from procfs * * \return true if process ID information is available, otherwise false */ bool pcmk__procfs_has_pids(void) { #if HAVE_LINUX_PROCFS static bool have_pids = false; static bool checked = false; if (!checked) { - char path[PATH_MAX]; - - have_pids = pcmk__procfs_pid2path(getpid(), path, sizeof(path)) == pcmk_rc_ok; + have_pids = pcmk__procfs_pid2path(getpid(), NULL) == pcmk_rc_ok; checked = true; } return have_pids; #else return false; #endif // HAVE_LINUX_PROCFS } /*! * \internal * \brief Return an open handle on the directory containing links to open file * descriptors, or NULL on error */ DIR * pcmk__procfs_fd_dir(void) { DIR *dir = NULL; /* /proc/self/fd (on Linux) or /dev/fd (on most OSes) contains symlinks to * all open files for the current process, named as the file descriptor. * Use this if available, because it's more efficient than a shotgun * approach to closing descriptors. */ #if HAVE_LINUX_PROCFS dir = opendir("/proc/self/fd"); #endif // HAVE_LINUX_PROCFS return dir; } /*! * \internal * \brief Trigger a sysrq command if supported on current platform * * \param[in] t Sysrq command to trigger */ void pcmk__sysrq_trigger(char t) { #if HAVE_LINUX_PROCFS // Root can always write here, regardless of kernel.sysrq value FILE *procf = fopen("/proc/sysrq-trigger", "a"); if (procf == NULL) { crm_warn("Could not open sysrq-trigger: %s", strerror(errno)); } else { fprintf(procf, "%c\n", t); fclose(procf); } #endif // HAVE_LINUX_PROCFS } bool pcmk__throttle_cib_load(const char *server, float *load) { /* /proc/[pid]/stat * * Status information about the process. This is used by ps(1). It is defined * in /usr/src/linux/fs/proc/array.c. * * The fields, in order, with their proper scanf(3) format specifiers, are: * * pid %d (1) The process ID. * comm %s (2) The filename of the executable, in parentheses. This is * visible whether or not the executable is swapped out. * state %c (3) One character from the string "RSDZTW" where R is running, * S is sleeping in an interruptible wait, D is waiting in * uninterruptible disk sleep, Z is zombie, T is traced or * stopped (on a signal), and W is paging. * ppid %d (4) The PID of the parent. * pgrp %d (5) The process group ID of the process. * session %d (6) The session ID of the process. * tty_nr %d (7) The controlling terminal of the process. (The minor device * number is contained in the combination of bits 31 to 20 and * 7 to 0; the major device number is in bits 15 to 8.) * tpgid %d (8) The ID of the foreground process group of the controlling * terminal of the process. * flags %u (9) The kernel flags word of the process. For bit meanings, see * the PF_* defines in the Linux kernel source file include/linux/sched.h. * Details depend on the kernel version. * minflt %lu (10) The number of minor faults the process has made which have * not required loading a memory page from disk. * cminflt %lu (11) The number of minor faults that the process's waited-for * children have made. * majflt %lu (12) The number of major faults the process has made which have * required loading a memory page from disk. * cmajflt %lu (13) The number of major faults that the process's waited-for * children have made. * utime %lu (14) Amount of time that this process has been scheduled in user * mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). * This includes guest time, guest_time (time spent running a * virtual CPU, see below), so that applications that are not * aware of the guest time field do not lose that time from * their calculations. * stime %lu (15) Amount of time that this process has been scheduled in * kernel mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). */ #if HAVE_LINUX_PROCFS static char *loadfile = NULL; static time_t last_call = 0; static long ticks_per_s = 0; static unsigned long last_utime, last_stime; char buffer[64*1024]; FILE *stream = NULL; time_t now = time(NULL); if (load == NULL) { return false; } else { *load = 0.0; } if (loadfile == NULL) { last_call = 0; last_utime = 0; last_stime = 0; loadfile = find_cib_loadfile(server); if (loadfile == NULL) { crm_warn("Couldn't find CIB load file"); return false; } ticks_per_s = sysconf(_SC_CLK_TCK); crm_trace("Found %s", loadfile); } stream = fopen(loadfile, "r"); if (stream == NULL) { int rc = errno; crm_warn("Couldn't read %s: %s (%d)", loadfile, pcmk_rc_str(rc), rc); free(loadfile); loadfile = NULL; return false; } if (fgets(buffer, sizeof(buffer), stream) != NULL) { char *comm = pcmk__assert_alloc(1, 256); char state = 0; int rc = 0, pid = 0, ppid = 0, pgrp = 0, session = 0, tty_nr = 0, tpgid = 0; unsigned long flags = 0, minflt = 0, cminflt = 0, majflt = 0, cmajflt = 0, utime = 0, stime = 0; rc = sscanf(buffer, "%d %[^ ] %c %d %d %d %d %d %lu %lu %lu %lu %lu %lu %lu", &pid, comm, &state, &ppid, &pgrp, &session, &tty_nr, &tpgid, &flags, &minflt, &cminflt, &majflt, &cmajflt, &utime, &stime); free(comm); if (rc != 15) { crm_err("Only %d of 15 fields found in %s", rc, loadfile); fclose(stream); return false; } else if ((last_call > 0) && (last_call < now) && (last_utime <= utime) && (last_stime <= stime)) { time_t elapsed = now - last_call; unsigned long delta_utime = utime - last_utime; unsigned long delta_stime = stime - last_stime; *load = delta_utime + delta_stime; /* Cast to a float before division */ *load /= ticks_per_s; *load /= elapsed; crm_debug("cib load: %f (%lu ticks in %lds)", *load, delta_utime + delta_stime, (long) elapsed); } else { crm_debug("Init %lu + %lu ticks at %ld (%lu tps)", utime, stime, (long) now, ticks_per_s); } last_call = now; last_utime = utime; last_stime = stime; fclose(stream); return true; } fclose(stream); #endif // HAVE_LINUX_PROCFS return false; } bool pcmk__throttle_load_avg(float *load) { #if HAVE_LINUX_PROCFS char buffer[256]; FILE *stream = NULL; const char *loadfile = "/proc/loadavg"; if (load == NULL) { return false; } stream = fopen(loadfile, "r"); if (stream == NULL) { int rc = errno; crm_warn("Couldn't read %s: %s (%d)", loadfile, pcmk_rc_str(rc), rc); return false; } if (fgets(buffer, sizeof(buffer), stream) != NULL) { char *nl = strstr(buffer, "\n"); /* Grab the 1-minute average, ignore the rest */ *load = strtof(buffer, NULL); if (nl != NULL) { nl[0] = 0; } fclose(stream); return true; } fclose(stream); #endif // HAVE_LINUX_PROCFS return false; } diff --git a/lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c b/lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c index 9fc73bc5df..2f66bf83d0 100644 --- a/lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c +++ b/lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c @@ -1,40 +1,39 @@ /* * Copyright 2022-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include "mock_private.h" #include #include #include static void no_pids(void **state) { char *exe_path = crm_strdup_printf("/proc/%lld/exe", (long long) getpid()); // Set readlink() errno and link contents (for /proc/PID/exe) pcmk__mock_readlink = true; expect_string(__wrap_readlink, path, exe_path); - expect_any(__wrap_readlink, buf); - expect_value(__wrap_readlink, bufsize, PATH_MAX - 1); + expect_value(__wrap_readlink, bufsize, PATH_MAX); will_return(__wrap_readlink, ENOENT); will_return(__wrap_readlink, NULL); assert_false(pcmk__procfs_has_pids()); pcmk__mock_readlink = false; free(exe_path); } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(no_pids)) diff --git a/lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c b/lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c index 65a4a320bd..e1a26c0984 100644 --- a/lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c +++ b/lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c @@ -1,40 +1,39 @@ /* * Copyright 2022-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include "mock_private.h" #include #include #include static void has_pids(void **state) { char *exe_path = crm_strdup_printf("/proc/%lld/exe", (long long) getpid()); // Set readlink() errno and link contents (for /proc/PID/exe) pcmk__mock_readlink = true; expect_string(__wrap_readlink, path, exe_path); - expect_any(__wrap_readlink, buf); - expect_value(__wrap_readlink, bufsize, PATH_MAX - 1); + expect_value(__wrap_readlink, bufsize, PATH_MAX); will_return(__wrap_readlink, 0); will_return(__wrap_readlink, "/ok"); assert_true(pcmk__procfs_has_pids()); pcmk__mock_readlink = false; free(exe_path); } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(has_pids)) diff --git a/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c index 52fe006bb3..c1ea5edb4c 100644 --- a/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c +++ b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c @@ -1,92 +1,87 @@ /* - * Copyright 2022-2024 the Pacemaker project contributors + * Copyright 2022-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include "mock_private.h" #include #include #include static void -no_exe_file(void **state) +assert_pid2path_one(int errno_to_set, const char *link_contents, char **dest, + int reference_rc) { - size_t len = PATH_MAX; - char *path = pcmk__assert_alloc(len, sizeof(char)); - - // Set readlink() errno and link contents pcmk__mock_readlink = true; expect_string(__wrap_readlink, path, "/proc/1000/exe"); - expect_value(__wrap_readlink, buf, path); - expect_value(__wrap_readlink, bufsize, len - 1); - will_return(__wrap_readlink, ENOENT); - will_return(__wrap_readlink, NULL); + expect_value(__wrap_readlink, bufsize, PATH_MAX); + will_return(__wrap_readlink, errno_to_set); + will_return(__wrap_readlink, link_contents); - assert_int_equal(pcmk__procfs_pid2path(1000, path, len), ENOENT); + assert_int_equal(pcmk__procfs_pid2path(1000, dest), reference_rc); pcmk__mock_readlink = false; - - free(path); } static void -contents_too_long(void **state) +assert_pid2path(int errno_to_set, const char *link_contents, int reference_rc, + const char *reference_result) { - size_t len = 10; - char *path = pcmk__assert_alloc(len, sizeof(char)); + char *dest = NULL; - // Set readlink() errno and link contents - pcmk__mock_readlink = true; + assert_pid2path_one(errno_to_set, link_contents, NULL, reference_rc); + assert_pid2path_one(errno_to_set, link_contents, &dest, reference_rc); - expect_string(__wrap_readlink, path, "/proc/1000/exe"); - expect_value(__wrap_readlink, buf, path); - expect_value(__wrap_readlink, bufsize, len - 1); - will_return(__wrap_readlink, 0); - will_return(__wrap_readlink, "/more/than/10/characters"); + if (reference_result == NULL) { + assert_null(dest); + } else { + assert_string_equal(dest, reference_result); + free(dest); + } +} - assert_int_equal(pcmk__procfs_pid2path(1000, path, len), - ENAMETOOLONG); +static void +no_exe_file(void **state) +{ + assert_pid2path(ENOENT, NULL, ENOENT, NULL); +} - pcmk__mock_readlink = false; +static void +contents_too_long(void **state) +{ + // String length equals PATH_MAX + char *long_path = crm_strdup_printf("%0*d", PATH_MAX, 0); - free(path); + assert_pid2path(0, long_path, ENAMETOOLONG, NULL); + free(long_path); } static void contents_ok(void **state) { - size_t len = PATH_MAX; - char *path = pcmk__assert_alloc(len, sizeof(char)); + char *real_path = pcmk__str_copy("/ok"); - // Set readlink() errno and link contents - pcmk__mock_readlink = true; - - expect_string(__wrap_readlink, path, "/proc/1000/exe"); - expect_value(__wrap_readlink, buf, path); - expect_value(__wrap_readlink, bufsize, len - 1); - will_return(__wrap_readlink, 0); - will_return(__wrap_readlink, "/ok"); + assert_pid2path(0, real_path, pcmk_rc_ok, real_path); + free(real_path); - assert_int_equal(pcmk__procfs_pid2path((pid_t) 1000, path, len), - pcmk_rc_ok); - assert_string_equal(path, "/ok"); - - pcmk__mock_readlink = false; + // String length equals PATH_MAX - 1 + real_path = crm_strdup_printf("%0*d", PATH_MAX - 1, 0); - free(path); + assert_pid2path(0, real_path, pcmk_rc_ok, real_path); + free(real_path); } PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(no_exe_file), cmocka_unit_test(contents_too_long), cmocka_unit_test(contents_ok))