diff options
Diffstat (limited to 'lib/common/io.c')
-rw-r--r-- | lib/common/io.c | 663 |
1 files changed, 663 insertions, 0 deletions
diff --git a/lib/common/io.c b/lib/common/io.c new file mode 100644 index 0000000..2264e16 --- /dev/null +++ b/lib/common/io.c @@ -0,0 +1,663 @@ +/* + * 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 Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include <crm_internal.h> + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include <sys/param.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/resource.h> + +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <stdlib.h> +#include <fcntl.h> +#include <dirent.h> +#include <errno.h> +#include <limits.h> +#include <pwd.h> +#include <grp.h> + +#include <crm/crm.h> +#include <crm/common/util.h> + +/*! + * \internal + * \brief Create a directory, including any parent directories needed + * + * \param[in] path_c Pathname of the directory to create + * \param[in] mode Permissions to be used (with current umask) when creating + * + * \return Standard Pacemaker return code + */ +int +pcmk__build_path(const char *path_c, mode_t mode) +{ + int offset = 1, len = 0; + int rc = pcmk_rc_ok; + char *path = strdup(path_c); + + // cppcheck seems not to understand the abort logic in CRM_CHECK + // cppcheck-suppress memleak + CRM_CHECK(path != NULL, return -ENOMEM); + for (len = strlen(path); offset < len; offset++) { + if (path[offset] == '/') { + path[offset] = 0; + if ((mkdir(path, mode) < 0) && (errno != EEXIST)) { + rc = errno; + goto done; + } + path[offset] = '/'; + } + } + if ((mkdir(path, mode) < 0) && (errno != EEXIST)) { + rc = errno; + } +done: + free(path); + return rc; +} + +/*! + * \internal + * \brief Return canonicalized form of a path name + * + * \param[in] path Pathname to canonicalize + * \param[out] resolved_path Where to store canonicalized pathname + * + * \return Standard Pacemaker return code + * \note The caller is responsible for freeing \p resolved_path on success. + * \note This function exists because not all C library versions of + * realpath(path, resolved_path) support a NULL resolved_path. + */ +int +pcmk__real_path(const char *path, char **resolved_path) +{ + CRM_CHECK((path != NULL) && (resolved_path != NULL), return EINVAL); + +#if _POSIX_VERSION >= 200809L + /* Recent C libraries can dynamically allocate memory as needed */ + *resolved_path = realpath(path, NULL); + return (*resolved_path == NULL)? errno : pcmk_rc_ok; + +#elif defined(PATH_MAX) + /* Older implementations require pre-allocated memory */ + /* (this is less desirable because PATH_MAX may be huge or not defined) */ + *resolved_path = malloc(PATH_MAX); + if ((*resolved_path == NULL) || (realpath(path, *resolved_path) == NULL)) { + return errno; + } + return pcmk_rc_ok; +#else + *resolved_path = NULL; + return ENOTSUP; +#endif +} + +/*! + * \internal + * \brief Create a file name using a sequence number + * + * \param[in] directory Directory that contains the file series + * \param[in] series Start of file name + * \param[in] sequence Sequence number + * \param[in] bzip Whether to use ".bz2" instead of ".raw" as extension + * + * \return Newly allocated file path (asserts on error, so always non-NULL) + * \note The caller is responsible for freeing the return value. + */ +char * +pcmk__series_filename(const char *directory, const char *series, int sequence, + bool bzip) +{ + CRM_ASSERT((directory != NULL) && (series != NULL)); + return crm_strdup_printf("%s/%s-%d.%s", directory, series, sequence, + (bzip? "bz2" : "raw")); +} + +/*! + * \internal + * \brief Read sequence number stored in a file series' .last file + * + * \param[in] directory Directory that contains the file series + * \param[in] series Start of file name + * \param[out] seq Where to store the sequence number + * + * \return Standard Pacemaker return code + */ +int +pcmk__read_series_sequence(const char *directory, const char *series, + unsigned int *seq) +{ + int rc; + FILE *fp = NULL; + char *series_file = NULL; + + if ((directory == NULL) || (series == NULL) || (seq == NULL)) { + return EINVAL; + } + + series_file = crm_strdup_printf("%s/%s.last", directory, series); + fp = fopen(series_file, "r"); + if (fp == NULL) { + rc = errno; + crm_debug("Could not open series file %s: %s", + series_file, strerror(rc)); + free(series_file); + return rc; + } + errno = 0; + if (fscanf(fp, "%u", seq) != 1) { + rc = (errno == 0)? ENODATA : errno; + crm_debug("Could not read sequence number from series file %s: %s", + series_file, pcmk_rc_str(rc)); + fclose(fp); + return rc; + } + fclose(fp); + crm_trace("Found last sequence number %u in series file %s", + *seq, series_file); + free(series_file); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Write sequence number to a file series' .last file + * + * \param[in] directory Directory that contains the file series + * \param[in] series Start of file name + * \param[in] sequence Sequence number to write + * \param[in] max Maximum sequence value, after which it is reset to 0 + * + * \note This function logs some errors but does not return any to the caller + */ +void +pcmk__write_series_sequence(const char *directory, const char *series, + unsigned int sequence, int max) +{ + int rc = 0; + FILE *file_strm = NULL; + char *series_file = NULL; + + CRM_CHECK(directory != NULL, return); + CRM_CHECK(series != NULL, return); + + if (max == 0) { + return; + } + if (max > 0 && sequence >= max) { + sequence = 0; + } + + series_file = crm_strdup_printf("%s/%s.last", directory, series); + file_strm = fopen(series_file, "w"); + if (file_strm != NULL) { + rc = fprintf(file_strm, "%u", sequence); + if (rc < 0) { + crm_perror(LOG_ERR, "Cannot write to series file %s", series_file); + } + + } else { + crm_err("Cannot open series file %s for writing", series_file); + } + + if (file_strm != NULL) { + fflush(file_strm); + fclose(file_strm); + } + + crm_trace("Wrote %d to %s", sequence, series_file); + free(series_file); +} + +/*! + * \internal + * \brief Change the owner and group of a file series' .last file + * + * \param[in] directory Directory that contains series + * \param[in] series Series to change + * \param[in] uid User ID of desired file owner + * \param[in] gid Group ID of desired file group + * + * \return Standard Pacemaker return code + * \note The caller must have the appropriate privileges. + */ +int +pcmk__chown_series_sequence(const char *directory, const char *series, + uid_t uid, gid_t gid) +{ + char *series_file = NULL; + int rc = pcmk_rc_ok; + + if ((directory == NULL) || (series == NULL)) { + return EINVAL; + } + series_file = crm_strdup_printf("%s/%s.last", directory, series); + if (chown(series_file, uid, gid) < 0) { + rc = errno; + } + free(series_file); + return rc; +} + +static bool +pcmk__daemon_user_can_write(const char *target_name, struct stat *target_stat) +{ + struct passwd *sys_user = NULL; + + errno = 0; + sys_user = getpwnam(CRM_DAEMON_USER); + if (sys_user == NULL) { + crm_notice("Could not find user %s: %s", + CRM_DAEMON_USER, pcmk_rc_str(errno)); + return FALSE; + } + if (target_stat->st_uid != sys_user->pw_uid) { + crm_notice("%s is not owned by user %s " CRM_XS " uid %d != %d", + target_name, CRM_DAEMON_USER, sys_user->pw_uid, + target_stat->st_uid); + return FALSE; + } + if ((target_stat->st_mode & (S_IRUSR | S_IWUSR)) == 0) { + crm_notice("%s is not readable and writable by user %s " + CRM_XS " st_mode=0%lo", + target_name, CRM_DAEMON_USER, + (unsigned long) target_stat->st_mode); + return FALSE; + } + return TRUE; +} + +static bool +pcmk__daemon_group_can_write(const char *target_name, struct stat *target_stat) +{ + struct group *sys_grp = NULL; + + errno = 0; + sys_grp = getgrnam(CRM_DAEMON_GROUP); + if (sys_grp == NULL) { + crm_notice("Could not find group %s: %s", + CRM_DAEMON_GROUP, pcmk_rc_str(errno)); + return FALSE; + } + + if (target_stat->st_gid != sys_grp->gr_gid) { + crm_notice("%s is not owned by group %s " CRM_XS " uid %d != %d", + target_name, CRM_DAEMON_GROUP, + sys_grp->gr_gid, target_stat->st_gid); + return FALSE; + } + + if ((target_stat->st_mode & (S_IRGRP | S_IWGRP)) == 0) { + crm_notice("%s is not readable and writable by group %s " + CRM_XS " st_mode=0%lo", + target_name, CRM_DAEMON_GROUP, + (unsigned long) target_stat->st_mode); + return FALSE; + } + return TRUE; +} + +/*! + * \internal + * \brief Check whether a directory or file is writable by the cluster daemon + * + * Return true if either the cluster daemon user or cluster daemon group has + * write permission on a specified file or directory. + * + * \param[in] dir Directory to check (this argument must be specified, and + * the directory must exist) + * \param[in] file File to check (only the directory will be checked if this + * argument is not specified or the file does not exist) + * + * \return true if target is writable by cluster daemon, false otherwise + */ +bool +pcmk__daemon_can_write(const char *dir, const char *file) +{ + int s_res = 0; + struct stat buf; + char *full_file = NULL; + const char *target = NULL; + + // Caller must supply directory + CRM_ASSERT(dir != NULL); + + // If file is given, check whether it exists as a regular file + if (file != NULL) { + full_file = crm_strdup_printf("%s/%s", dir, file); + target = full_file; + + s_res = stat(full_file, &buf); + if (s_res < 0) { + crm_notice("%s not found: %s", target, pcmk_rc_str(errno)); + free(full_file); + full_file = NULL; + target = NULL; + + } else if (S_ISREG(buf.st_mode) == FALSE) { + crm_err("%s must be a regular file " CRM_XS " st_mode=0%lo", + target, (unsigned long) buf.st_mode); + free(full_file); + return false; + } + } + + // If file is not given, ensure dir exists as directory + if (target == NULL) { + target = dir; + s_res = stat(dir, &buf); + if (s_res < 0) { + crm_err("%s not found: %s", dir, pcmk_rc_str(errno)); + return false; + + } else if (S_ISDIR(buf.st_mode) == FALSE) { + crm_err("%s must be a directory " CRM_XS " st_mode=0%lo", + dir, (unsigned long) buf.st_mode); + return false; + } + } + + if (!pcmk__daemon_user_can_write(target, &buf) + && !pcmk__daemon_group_can_write(target, &buf)) { + + crm_err("%s must be owned and writable by either user %s or group %s " + CRM_XS " st_mode=0%lo", + target, CRM_DAEMON_USER, CRM_DAEMON_GROUP, + (unsigned long) buf.st_mode); + free(full_file); + return false; + } + + free(full_file); + return true; +} + +/*! + * \internal + * \brief Flush and sync a directory to disk + * + * \param[in] name Directory to flush and sync + * \note This function logs errors but does not return them to the caller + */ +void +pcmk__sync_directory(const char *name) +{ + int fd; + DIR *directory; + + directory = opendir(name); + if (directory == NULL) { + crm_perror(LOG_ERR, "Could not open %s for syncing", name); + return; + } + + fd = dirfd(directory); + if (fd < 0) { + crm_perror(LOG_ERR, "Could not obtain file descriptor for %s", name); + return; + } + + if (fsync(fd) < 0) { + crm_perror(LOG_ERR, "Could not sync %s", name); + } + if (closedir(directory) < 0) { + crm_perror(LOG_ERR, "Could not close %s after fsync", name); + } +} + +/*! + * \internal + * \brief Read the contents of a file + * + * \param[in] filename Name of file to read + * \param[out] contents Where to store file contents + * + * \return Standard Pacemaker return code + * \note On success, the caller is responsible for freeing contents. + */ +int +pcmk__file_contents(const char *filename, char **contents) +{ + FILE *fp; + int length, read_len; + int rc = pcmk_rc_ok; + + if ((filename == NULL) || (contents == NULL)) { + return EINVAL; + } + + fp = fopen(filename, "r"); + if ((fp == NULL) || (fseek(fp, 0L, SEEK_END) < 0)) { + rc = errno; + goto bail; + } + + length = ftell(fp); + if (length < 0) { + rc = errno; + goto bail; + } + + if (length == 0) { + *contents = NULL; + } else { + *contents = calloc(length + 1, sizeof(char)); + if (*contents == NULL) { + rc = errno; + goto bail; + } + rewind(fp); + read_len = fread(*contents, 1, length, fp); /* Coverity: False positive */ + if (read_len != length) { + free(*contents); + *contents = NULL; + rc = EIO; + } + } + +bail: + if (fp != NULL) { + fclose(fp); + } + return rc; +} + +/*! + * \internal + * \brief Write text to a file, flush and sync it to disk, then close the file + * + * \param[in] fd File descriptor opened for writing + * \param[in] contents String to write to file + * + * \return Standard Pacemaker return code + */ +int +pcmk__write_sync(int fd, const char *contents) +{ + int rc = 0; + FILE *fp = fdopen(fd, "w"); + + if (fp == NULL) { + return errno; + } + if ((contents != NULL) && (fprintf(fp, "%s", contents) < 0)) { + rc = EIO; + } + if (fflush(fp) != 0) { + rc = errno; + } + if (fsync(fileno(fp)) < 0) { + rc = errno; + } + fclose(fp); + return rc; +} + +/*! + * \internal + * \brief Set a file descriptor to non-blocking + * + * \param[in] fd File descriptor to use + * + * \return Standard Pacemaker return code + */ +int +pcmk__set_nonblocking(int fd) +{ + int flag = fcntl(fd, F_GETFL); + + if (flag < 0) { + return errno; + } + if (fcntl(fd, F_SETFL, flag | O_NONBLOCK) < 0) { + return errno; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Get directory name for temporary files + * + * Return the value of the TMPDIR environment variable if it is set to a + * full path, otherwise return "/tmp". + * + * \return Name of directory to be used for temporary files + */ +const char * +pcmk__get_tmpdir(void) +{ + const char *dir = getenv("TMPDIR"); + + return (dir && (*dir == '/'))? dir : "/tmp"; +} + +/*! + * \internal + * \brief Close open file descriptors + * + * Close all file descriptors (except optionally stdin, stdout, and stderr), + * which is a best practice for a new child process forked for the purpose of + * executing an external program. + * + * \param[in] bool If true, close stdin, stdout, and stderr as well + */ +void +pcmk__close_fds_in_child(bool all) +{ + DIR *dir; + struct rlimit rlim; + rlim_t max_fd; + int min_fd = (all? 0 : (STDERR_FILENO + 1)); + + /* Find the current process's (soft) limit for open files. getrlimit() + * should always work, but have a fallback just in case. + */ + if (getrlimit(RLIMIT_NOFILE, &rlim) == 0) { + max_fd = rlim.rlim_cur - 1; + } else { + long conf_max = sysconf(_SC_OPEN_MAX); + + max_fd = (conf_max > 0)? conf_max : 1024; + } + + /* /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"); + if (dir == NULL) { + dir = opendir("/dev/fd"); + } +#else + dir = opendir("/dev/fd"); +#endif // HAVE_LINUX_PROCFS + if (dir != NULL) { + struct dirent *entry; + int dir_fd = dirfd(dir); + + while ((entry = readdir(dir)) != NULL) { + int lpc = atoi(entry->d_name); + + /* How could one of these entries be higher than max_fd, you ask? + * It isn't possible in normal operation, but when run under + * valgrind, valgrind can open high-numbered file descriptors for + * its own use that are higher than the process's soft limit. + * These will show up in the fd directory but aren't closable. + */ + if ((lpc >= min_fd) && (lpc <= max_fd) && (lpc != dir_fd)) { + close(lpc); + } + } + closedir(dir); + return; + } + + /* If no fd directory is available, iterate over all possible descriptors. + * This is less efficient due to the overhead of many system calls. + */ + for (int lpc = max_fd; lpc >= min_fd; lpc--) { + close(lpc); + } +} + +/*! + * \brief Duplicate a file path, inserting a prefix if not absolute + * + * \param[in] filename File path to duplicate + * \param[in] dirname If filename is not absolute, prefix to add + * + * \return Newly allocated memory with full path (guaranteed non-NULL) + */ +char * +pcmk__full_path(const char *filename, const char *dirname) +{ + char *path = NULL; + + CRM_ASSERT(filename != NULL); + + if (filename[0] == '/') { + path = strdup(filename); + CRM_ASSERT(path != NULL); + + } else { + CRM_ASSERT(dirname != NULL); + path = crm_strdup_printf("%s/%s", dirname, filename); + } + + return path; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include <crm/common/util_compat.h> + +void +crm_build_path(const char *path_c, mode_t mode) +{ + int rc = pcmk__build_path(path_c, mode); + + if (rc != pcmk_rc_ok) { + crm_err("Could not create directory '%s': %s", + path_c, pcmk_rc_str(rc)); + } +} + +// LCOV_EXCL_STOP +// End deprecated API |