// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2026 Andrea Mazzoleni #include "os/portable.h" #ifndef __MINGW32__ /* only for Unix */ #include "os.h" /****************************************************************************/ /* signal */ void os_signal_restore_after_fork(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_DFL; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGTERM, &sa, 0); sigaction(SIGINT, &sa, 0); sigaction(SIGHUP, &sa, 0); /* do not restore SIGPIPE */ /* ensure signals are unblocked */ sigset_t mask; sigemptyset(&mask); sigprocmask(SIG_SETMASK, &mask, NULL); /* cannot use pthread_sigmask after fork */ } void os_signal_init(void (*handler_term)(int sig), void (*handler_hup)(int sig)) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = handler_term; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; /* use the SA_RESTART to automatically restart interrupted system calls */ sigaction(SIGTERM, &sa, 0); sigaction(SIGINT, &sa, 0); sa.sa_handler = handler_hup; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; /* use the SA_RESTART to automatically restart interrupted system calls */ sigaction(SIGHUP, &sa, 0); sa.sa_handler = SIG_IGN; /* ignore the signal */ sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGPIPE, &sa, 0); } void os_signal_set(int enable) { sigset_t set; sigemptyset(&set); sigaddset(&set, SIGTERM); sigaddset(&set, SIGINT); sigaddset(&set, SIGHUP); pthread_sigmask(enable ? SIG_UNBLOCK : SIG_BLOCK, &set, 0); } const char* os_signal_name(int sig) { switch (sig) { #ifdef SIGHUP case SIGHUP : return "SIGHUP"; #endif #ifdef SIGINT case SIGINT : return "SIGINT"; #endif #ifdef SIGQUIT case SIGQUIT : return "SIGQUIT"; #endif #ifdef SIGILL case SIGILL : return "SIGILL"; #endif #ifdef SIGTRAP case SIGTRAP : return "SIGTRAP"; #endif #ifdef SIGABRT case SIGABRT : return "SIGABRT"; #endif #ifdef SIGBUS case SIGBUS : return "SIGBUS"; #endif #ifdef SIGFPE case SIGFPE : return "SIGFPE"; #endif #ifdef SIGKILL case SIGKILL : return "SIGKILL"; #endif #ifdef SIGUSR1 case SIGUSR1 : return "SIGUSR1"; #endif #ifdef SIGSEGV case SIGSEGV : return "SIGSEGV"; #endif #ifdef SIGUSR2 case SIGUSR2 : return "SIGUSR2"; #endif #ifdef SIGPIPE case SIGPIPE : return "SIGPIPE"; #endif #ifdef SIGALRM case SIGALRM : return "SIGALRM"; #endif #ifdef SIGTERM case SIGTERM : return "SIGTERM"; #endif } return "UNKNOWN"; } /****************************************************************************/ /* fs */ char* absolutepath(const char* restrict path, char* restrict resolved_path) { if (path == 0 || path[0] == 0 || resolved_path == 0) { errno = EINVAL; return 0; } size_t path_len = strlen(path); if (path_len >= PATH_MAX) { errno = ENAMETOOLONG; return 0; } if (path[0] == '/') { memcpy(resolved_path, path, path_len + 1); return resolved_path; } if (getcwd(resolved_path, PATH_MAX) == 0) return 0; size_t resolved_len = strlen(resolved_path); int separator = resolved_len != 0 && resolved_path[resolved_len - 1] != '/'; if (resolved_len + separator + path_len + 1 > PATH_MAX) { errno = ENAMETOOLONG; return 0; } if (separator) resolved_path[resolved_len++] = '/'; memcpy(resolved_path + resolved_len, path, path_len + 1); return resolved_path; } #if !HAVE_EACCESS int eaccess(const char* pathname, int mode) { return faccessat(AT_FDCWD, pathname, mode, AT_EACCESS); } #endif #if HAVE_POSIX_FADVISE int posix_fadvise_wrapper(int fd, off_t offset, off_t len, int advice) { int ret = posix_fadvise(fd, offset, len, advice); if (ret == ENOENT) return ENOSYS; return ret; } #endif int open_noatime(const char* file, int flags) { #ifdef O_NOATIME int f = open(file, flags | O_NOATIME); /* only root is allowed to use O_NOATIME, in case retry without it */ if (f == -1 && errno == EPERM) f = open(file, flags); return f; #else return open(file, flags); #endif } int dirent_hidden(struct dirent* dd) { return dd->d_name[0] == '.'; } size_t direct_size(void) { long size; size = sysconf(_SC_PAGESIZE); if (size == -1) { /* LCOV_EXCL_START */ os_syslog(OS_LVL_CRITICAL, "no page size"); os_exit(); /* LCOV_EXCL_STOP */ } return size; } const char* stat_desc(struct stat* st) { if (S_ISREG(st->st_mode)) return "regular"; if (S_ISDIR(st->st_mode)) return "directory"; if (S_ISCHR(st->st_mode)) return "character"; if (S_ISBLK(st->st_mode)) return "block-device"; if (S_ISFIFO(st->st_mode)) return "fifo"; if (S_ISLNK(st->st_mode)) return "symlink"; if (S_ISSOCK(st->st_mode)) return "socket"; return "unknown"; } int filephy(const char* path, uint64_t size, uint64_t* physical) { #if HAVE_LINUX_FIEMAP_H /* * In Linux get the real physical address of the file * Note that FIEMAP doesn't require root permission */ int f; struct fiemap* fiemap; size_t fiemap_size; unsigned int blknum; /* * First try with FIEMAP * if works for ext2, ext3, ext4, xfs, btrfs */ fiemap_size = sizeof(struct fiemap) + sizeof(struct fiemap_extent); fiemap = malloc(fiemap_size); if (!fiemap) { /* LCOV_EXCL_START */ return -1; /* LCOV_EXCL_STOP */ } f = open(path, O_RDONLY | O_CLOEXEC); if (f == -1) { /* LCOV_EXCL_START */ free(fiemap); return -1; /* LCOV_EXCL_STOP */ } memset(fiemap, 0, fiemap_size); fiemap->fm_start = 0; fiemap->fm_length = ~0ULL; fiemap->fm_flags = FIEMAP_FLAG_SYNC; /* required to ensure that just created files report a valid address and not 0 */ fiemap->fm_extent_count = 1; /* we are interested only at the first block */ if (ioctl(f, FS_IOC_FIEMAP, fiemap) != -1) { uint32_t flags = fiemap->fm_extents[0].fe_flags; uint64_t offset = fiemap->fm_extents[0].fe_physical; /* check some condition for validating the offset */ if (flags & FIEMAP_EXTENT_DATA_INLINE) { /* if the data is inline, we don't have an offset to report */ *physical = FILEPHY_WITHOUT_OFFSET; } else if (flags & FIEMAP_EXTENT_UNKNOWN) { /* if the offset is unknown, we don't have an offset to report */ *physical = FILEPHY_WITHOUT_OFFSET; } else if (offset == 0) { /* * 0 is the general fallback for file-systems when * they don't have an offset to report */ *physical = FILEPHY_WITHOUT_OFFSET; } else { /* finally report the real offset */ *physical = offset + FILEPHY_REAL_OFFSET; } free(fiemap); if (close(f) == -1) return -1; return 0; } free(fiemap); /* if the file is empty, FIBMAP doesn't work, and we don't even try to use it */ if (size == 0) { *physical = FILEPHY_WITHOUT_OFFSET; if (close(f) == -1) return -1; return 0; } /* * Then try with FIBMAP * it works for jfs, reiserfs, ntfs-3g * in exfat it always returns 0, that it's anyway better than the fake inodes */ blknum = 0; /* first block */ if (ioctl(f, FIBMAP, &blknum) != -1) { *physical = blknum + FILEPHY_REAL_OFFSET; if (close(f) == -1) return -1; return 0; } /* * Otherwise don't use anything, and keep the directory traversal order * at now this should happen only for vfat * and it's surely better than using fake inodes */ *physical = FILEPHY_UNREPORTED_OFFSET; if (close(f) == -1) return -1; #else /* * In a generic Unix use a dummy value for all the files * We don't want to risk to use the inode without knowing * if it really improves performance. * In this way we keep them in the directory traversal order * that at least keeps files in the same directory together. * Note also that in newer file-system with snapshot, like ZFS, * the inode doesn't represent even more the disk position, because files * are not overwritten in place, but rewritten in another location * of the disk. */ *physical = FILEPHY_UNREPORTED_OFFSET; (void)path; /* not used here */ (void)size; #endif return 0; } /****************************************************************************/ /* exec */ /** * Scrubbed environment * Only provide the bare essentials. */ static char* const envp_scrubbed[] = { "PATH=" #ifdef __APPLE__ "/opt/homebrew/bin:" #endif "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "TERM=dumb", "LANG=C", "IFS= \t\n", NULL }; /** * Enforce a strict 128-byte shebang limit to match the actual Linux kernel behavior * and prevent security risks from truncation. * * The Linux kernel reads exactly 128 bytes (BINPRM_BUF_SIZE internal = 128) to parse * the shebang line. Longer lines are silently truncated, which can cause the kernel * to execute an unintended or malformed interpreter path. * * Although the UAPI header exposes 256 and other platforms (macOS, BSD) allow longer * lines, we deliberately enforce the conservative 128-byte limit on *all* platforms * to eliminate any possibility of a mismatch between our validation and runtime * execution on Linux, and to avoid subtle truncation-based attacks. * * Effective maximum shebang line length: ~126 characters (accounting for "#!", whitespace, * interpreter path, optional argument, and newline). */ #define SHEBANG_MAX (128 + 2) /* extra space for end-of-line and final 0 */ /** * Verifies that the shebang interpreter is in an allowed list of paths. * This prevents attacks where a malicious script uses an attacker-controlled interpreter. */ static int verify_shebang_interpreter(int fd, const char* script_path) { char shebang[SHEBANG_MAX]; ssize_t bytes_read; char* interpreter; char* args; struct stat st; /* list of allowed interpreter paths */ const char* allowed_interpreters[] = { /* shells - system */ "/bin/sh", "/usr/bin/sh", "/bin/bash", "/usr/bin/bash", "/bin/zsh", "/usr/bin/zsh", "/bin/dash", "/usr/bin/dash", /* shells - homebrew (Apple Silicon) */ #ifdef __APPLE__ "/opt/homebrew/bin/sh", "/opt/homebrew/bin/bash", "/opt/homebrew/bin/zsh", "/opt/homebrew/bin/dash", #endif /* shells - homebrew (Intel macOS legacy) */ "/usr/local/bin/bash", "/usr/local/bin/zsh", "/usr/local/bin/dash", /* python - system */ "/usr/bin/python3", "/usr/bin/python", /* python - Homebrew */ #ifdef __APPLE__ "/opt/homebrew/bin/python3", "/opt/homebrew/bin/python", #endif "/usr/local/bin/python3", "/usr/local/bin/python", /* perl */ "/usr/bin/perl", #ifdef __APPLE__ "/opt/homebrew/bin/perl", #endif "/usr/local/bin/perl", /* ruby */ "/usr/bin/ruby", #ifdef __APPLE__ "/opt/homebrew/bin/ruby", #endif "/usr/local/bin/ruby", /* node.js */ "/usr/bin/node", #ifdef __APPLE__ "/opt/homebrew/bin/node", #endif "/usr/local/bin/node", 0 }; bytes_read = pread(fd, shebang, sizeof(shebang) - 1, 0); /* reserve space for the terminating 0 */ if (bytes_read < 0) { os_syslog(OS_LVL_INFO, "failed to read script shebang, path=%s, errno=%s(%d)", script_path, strerror(errno), errno); return -1; } if (bytes_read < 4) { os_syslog(OS_LVL_INFO, "script %s is too small or missing a shebang", script_path); return -1; } shebang[bytes_read] = 0; /* check for shebang */ if (shebang[0] != '#' || shebang[1] != '!') { os_syslog(OS_LVL_INFO, "script %s is missing shebang (#!)", script_path); return -1; } char* end_of_line = strchr(shebang, '\n'); if (!end_of_line || end_of_line - shebang > 128) { os_syslog(OS_LVL_INFO, "script %s has invalid or overlong shebang (#!), exceeds 126 characters", script_path); return -1; } *end_of_line = 0; /* skip "#!" and whitespace */ interpreter = shebang + 2; while (*interpreter && isspace((unsigned char)*interpreter)) ++interpreter; if (*interpreter == 0) { os_syslog(OS_LVL_INFO, "script %s has empty shebang", script_path); return -1; } /* separate interpreter from arguments */ args = interpreter; while (*args && !isspace((unsigned char)*args)) ++args; if (*args) *args++ = 0; /* terminate interpreter */ /* check if interpreter is in allowed list */ int found = 0; for (int i = 0; allowed_interpreters[i] != 0; ++i) { if (strcmp(interpreter, allowed_interpreters[i]) == 0) { found = 1; break; } } if (!found) { os_syslog(OS_LVL_INFO, "script %s uses disallowed interpreter %s", script_path, interpreter); return -1; } /* verify interpreter exists and is safe */ if (stat(interpreter, &st) != 0) { os_syslog(OS_LVL_INFO, "interpreter %s does not exist, errno=%s(%d)", interpreter, strerror(errno), errno); return -1; } /* interpreter must be a regular file */ if (!S_ISREG(st.st_mode)) { os_syslog(OS_LVL_INFO, "interpreter %s must be a regular file", interpreter); return -1; } /* interpreter must be owned by root */ if (st.st_uid != 0) { os_syslog(OS_LVL_INFO, "interpreter %s not owned by root", interpreter); return -1; } /* interpreter must be not world-writable */ if (st.st_mode & S_IWOTH) { os_syslog(OS_LVL_INFO, "interpreter %s is world-writable", interpreter); return -1; } /* interpreter must be not group-writable (unless group is root) */ if ((st.st_mode & S_IWGRP) && st.st_gid != 0) { os_syslog(OS_LVL_INFO, "interpreter %s is group-writable by non-root group", interpreter); return -1; } /* interpreter must be executable */ if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) { os_syslog(OS_LVL_INFO, "interpreter %s is not executable", interpreter); return -1; } /* interpreter must be not setuid / setgid */ if (st.st_mode & (S_ISUID | S_ISGID)) { os_syslog(OS_LVL_INFO, "file %s has setuid/setgid bits set", interpreter); return -1; } /* all checks passed */ return 0; } /** * Securely verify and open an executable file. * * Performs a series of security checks on the file at @exec_path before * returning an open file descriptor suitable for use with fexecve(2), or * falling back to execve(2) on systems that lack it. * * Security model: * - @exec_path must be absolute. * - The path is resolved via realpath(3) to canonicalize it and eliminate * symlinks before any further checks are performed. * - The parent directory is opened with O_PATH | O_NOFOLLOW to pin its * inode, and the file is opened with openat(2) relative to that pinned * descriptor. This closes the TOCTOU window between path resolution * and file open. * - Both the parent directory and the file must be owned by root or the * daemon's real/effective UID, must not be world-writable, and must not * be group-writable by a group other than the daemon's real/effective GID. * - The file must be a regular file with at least one execute bit set. * - The setuid and setgid bits must not be set. * - On systems with fexecve(2) support, the returned fd is opened without * O_CLOEXEC if the file is a script (@is_script=1). This is necessary because * fexecve(2) executes scripts by passing a descriptor path (e.g., /dev/fd/N) * to the script interpreter, which requires the fd to remain open across * the execve syscall. For compiled binaries, O_CLOEXEC is safely applied * to prevent leaking the fd to the spawned process. On systems without * fexecve(2), execve(2) is used with @resolved_path and O_CLOEXEC is * always applied. * * @exec_path Absolute path to the executable to verify. * @resolved_path Caller-allocated buffer of at least PATH_MAX bytes. On * success, filled with the canonicalized path from realpath(3). * * Returns an open file descriptor (>= 0) on success. The caller is * responsible for closing it. Returns -1 on any verification failure; * the specific reason is emitted via os_syslog(OS_LVL_INFO, ...). */ static int verify_executable(const char* exec_path, char* resolved_path, int is_script) { struct stat st; uid_t process_uid, process_euid; gid_t process_gid, process_egid; process_uid = getuid(); process_euid = geteuid(); process_gid = getgid(); process_egid = getegid(); /* verify path is absolute */ if (exec_path[0] != '/') { os_syslog(OS_LVL_INFO, "path %s must be absolute", exec_path); return -1; } /* resolve the path to prevent symlink attacks */ if (!realpath(exec_path, resolved_path)) { os_syslog(OS_LVL_INFO, "failed to resolve %s, errno=%s(%d)", exec_path, strerror(errno), errno); return -1; } char* last_slash = strrchr(resolved_path, '/'); if (last_slash == 0) { os_syslog(OS_LVL_INFO, "relative execution of %s not allowed", resolved_path); return -1; } if (last_slash == resolved_path) { os_syslog(OS_LVL_INFO, "root dir execution of %s not allowed", resolved_path); return -1; } const char* exec_name = last_slash + 1; if (exec_name[0] == 0) { os_syslog(OS_LVL_INFO, "no executable name in %s", exec_path); return -1; } char dir_path[PATH_MAX]; size_t dir_len = last_slash - resolved_path; memcpy(dir_path, resolved_path, dir_len); dir_path[dir_len] = 0; int dir_fd = open(dir_path, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (dir_fd < 0) { os_syslog(OS_LVL_INFO, "failed to open directory %s, errno=%s(%d)", dir_path, strerror(errno), errno); return -1; } if (fstat(dir_fd, &st) != 0) { os_syslog(OS_LVL_INFO, "failed to stat directory %s, errno=%s(%d)", dir_path, strerror(errno), errno); close(dir_fd); return -1; } /* directory must be owned by root or the daemon's real user */ if (st.st_uid != process_uid && st.st_uid != process_euid && st.st_uid != 0) { os_syslog(OS_LVL_INFO, "directory %s owner must match the daemon owner or be root", dir_path); close(dir_fd); return -1; } /* directory must be not group-writable unless group matches daemon */ if ((st.st_mode & S_IWGRP) && st.st_gid != process_gid && st.st_gid != process_egid && st.st_gid != 0) { os_syslog(OS_LVL_INFO, "directory %s must be not group-writable unless group matches daemon owner or root", dir_path); close(dir_fd); return -1; } /* directory must be not world-writable */ if (st.st_mode & S_IWOTH) { os_syslog(OS_LVL_INFO, "directory %s must be not world-writable", dir_path); close(dir_fd); return -1; } /* * Open the executable */ int flags = O_RDONLY | O_NOFOLLOW; #if HAVE_FEXECVE if (!is_script) { flags |= O_CLOEXEC; /* with fexecve cannot use O_CLOEXEC (Close on Exec) for scripts */ } #else (void)is_script; flags |= O_CLOEXEC; #endif int fd = openat(dir_fd, exec_name, flags); if (fd < 0) { os_syslog(OS_LVL_INFO, "failed to open %s, errno=%s(%d)", resolved_path, strerror(errno), errno); close(dir_fd); return -1; } close(dir_fd); /* get the file handle (TOCTOU Protection) */ if (fstat(fd, &st) == -1) { os_syslog(OS_LVL_INFO, "failed to stat %s, errno=%s(%d)", resolved_path, strerror(errno), errno); close(fd); return -1; } /* ensure it's a regular file */ if (!S_ISREG(st.st_mode)) { os_syslog(OS_LVL_INFO, "file %s is not a regular file", resolved_path); close(fd); return -1; } /* explicitly reject scripts if not allowed */ if (!is_script) { char magic[2]; if (read(fd, magic, 2) == 2 && magic[0] == '#' && magic[1] == '!') { os_syslog(OS_LVL_INFO, "file %s is a script, which is not supported", resolved_path); close(fd); return -1; } if (lseek(fd, 0, SEEK_SET) == (off_t)-1) { /* ignore lseek errors on block devices or pipes, though it's a regular file here */ } } /* ensure it has execute permissions */ if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) { os_syslog(OS_LVL_INFO, "file %s is not an executable", resolved_path); close(fd); return -1; } /* must be owned by root or the daemon's real user */ if (st.st_uid != process_uid && st.st_uid != process_euid && st.st_uid != 0) { os_syslog(OS_LVL_INFO, "file %s owner must match the daemon owner or be root", resolved_path); close(fd); return -1; } /* must be not group-writable unless group matches daemon */ if ((st.st_mode & S_IWGRP) && st.st_gid != process_gid && st.st_gid != process_egid && st.st_gid != 0) { os_syslog(OS_LVL_INFO, "file %s must be not group-writable unless group matches daemon owner or root", resolved_path); close(fd); return -1; } /* must be not world-writable */ if (st.st_mode & S_IWOTH) { os_syslog(OS_LVL_INFO, "file %s must be not world-writable", resolved_path); close(fd); return -1; } /* must be not setuid / setgid */ if (st.st_mode & (S_ISUID | S_ISGID)) { os_syslog(OS_LVL_INFO, "file %s has setuid/setgid bits set", resolved_path); close(fd); return -1; } return fd; } int os_validate_exec_input(const char* str) { /* reject paths trying to go up levels */ if (strstr(str, "..") != 0) { os_syslog(OS_LVL_INFO, "invalid argument %s for execution", str); return -1; } /* reject inputs starting with '-' to prevent Flag Injection */ if (str[0] == '-') { os_syslog(OS_LVL_INFO, "invalid argument %s for execution", str); return -1; } return 0; } /****************************************************************************/ /* user credential management */ #define GROUPS_MAX 1024 #define PASSWD_MAX 8192 struct os_user { int enabled; uid_t uid; gid_t gid; int ngroups; gid_t groups[GROUPS_MAX]; }; #ifdef __APPLE__ typedef int os_group_t; #else typedef gid_t os_group_t; #endif static int os_user_lookup(const char* run_as_user, struct os_user* user) { memset(user, 0, sizeof(*user)); if (!run_as_user || run_as_user[0] == 0) return 0; struct passwd pw; struct passwd* pw_res = 0; char pw_buf[PASSWD_MAX]; int err = getpwnam_r(run_as_user, &pw, pw_buf, sizeof(pw_buf), &pw_res); if (err != 0 || !pw_res) { os_syslog(OS_LVL_INFO, "failed to get user info for %s, errno=%s(%d)", run_as_user, strerror(err ? err : ENOENT), err ? err : ENOENT); return -1; } user->enabled = 1; user->uid = pw.pw_uid; user->gid = pw.pw_gid; os_group_t groups_buf[GROUPS_MAX]; int ngroups = sizeof(groups_buf) / sizeof(groups_buf[0]); if (getgrouplist(pw.pw_name, (os_group_t)pw.pw_gid, groups_buf, &ngroups) < 0) { os_syslog(OS_LVL_INFO, "failed to get group list for %s, exceeds maximum of %d groups", run_as_user, GROUPS_MAX); return -1; } user->ngroups = ngroups; for (int i = 0; i < ngroups; ++i) { user->groups[i] = (gid_t)groups_buf[i]; } return 0; } #if defined(__linux__) #if defined(SYS_setgroups32) #define SYS_SETGROUPS SYS_setgroups32 #else #define SYS_SETGROUPS SYS_setgroups #endif #if defined(SYS_setgid32) #define SYS_SETGID SYS_setgid32 #else #define SYS_SETGID SYS_setgid #endif #if defined(SYS_setuid32) #define SYS_SETUID SYS_setuid32 #else #define SYS_SETUID SYS_setuid #endif #endif static void os_user_drop_privileges(const struct os_user* user) { if (user->enabled) { #if defined(__linux__) && defined(SYS_SETGROUPS) && defined(SYS_SETGID) && defined(SYS_SETUID) /* * This function is called only in the single-threaded child after fork(). * Use raw Linux syscalls to avoid the glibc NPTL setxid machinery. * Prefer the 32-bit credential syscalls on architectures that still * provide legacy 16-bit UID/GID syscall entry points. */ if (syscall(SYS_SETGROUPS, (size_t)user->ngroups, user->groups) != 0) _exit(126); if (syscall(SYS_SETGID, user->gid) != 0) _exit(126); if (syscall(SYS_SETUID, user->uid) != 0) _exit(126); #else if (setgroups(user->ngroups, user->groups) != 0) _exit(126); if (setgid(user->gid) != 0) _exit(126); if (setuid(user->uid) != 0) _exit(126); #endif } } static int pipe_cloexec(int pipefd[2]) { #ifdef HAVE_PIPE2 return pipe2(pipefd, O_CLOEXEC); #else if (pipe(pipefd) < 0) return -1; for (int i = 0; i < 2; i++) { int flags = fcntl(pipefd[i], F_GETFD); if (flags < 0) goto bail; if (fcntl(pipefd[i], F_SETFD, flags | FD_CLOEXEC) < 0) goto bail; } return 0; bail: close(pipefd[0]); close(pipefd[1]); return -1; #endif } #define PID_NONE ((pid_t)0) /**< No process is active. */ #define PID_SPAWN ((pid_t)-1) /**< Process creation is in progress. */ #define PID_TERM ((pid_t)-2) /**< Process termination was requested while creation is in progress. */ #define PID_KILL ((pid_t)-3) /**< Process kill was requested; rejects future spawns or terminates in-progress/active processes. */ static pthread_mutex_t exec_mutex = PTHREAD_MUTEX_INITIALIZER; /** * Acquire the process execution mutex. */ static void exec_lock(void) { pthread_mutex_lock(&exec_mutex); } /** * Release the process execution mutex. */ static void exec_unlock(void) { pthread_mutex_unlock(&exec_mutex); } /** * Forcibly terminate the specified raw process reference. */ static int pid_kill(pid_t pid) { if (pid <= 0) { errno = EINVAL; return -1; } /* send SIGKILL signal to the negative PID to target the entire Process Group */ int ret = kill(-pid, SIGKILL); /* * If setpgid(0, 0) has not yet executed in the child, process group -pid * does not exist and kill() returns ESRCH. Fallback to targeting the process directly. */ if (ret < 0 && errno == ESRCH) { ret = kill(pid, SIGKILL); } return ret; } /** * Gracefully terminate the specified raw process reference. */ static int pid_term(pid_t pid) { if (pid <= 0) { errno = EINVAL; return -1; } /* * Send signal to the negative PID to target the entire Process Group. * This ensures that SnapRAID and any programs it may have spawned are * terminated together, preventing orphaned worker processes. */ int ret = kill(-pid, SIGTERM); /* * If setpgid(0, 0) has not yet executed in the child, process group -pid * does not exist and kill() returns ESRCH. Fallback to targeting the process directly. */ if (ret < 0 && errno == ESRCH) { ret = kill(pid, SIGTERM); } return ret; } /** * Mark the start of process creation in a process-reference slot. */ static int pid_spawn_begin(pid_t* pid_slot) { if (pid_slot == 0) return 0; exec_lock(); if (*pid_slot == PID_KILL) { exec_unlock(); errno = ECANCELED; return -1; } *pid_slot = PID_SPAWN; exec_unlock(); return 0; } /** * Publish a newly created process reference, or terminate it immediately if canceled during spawn. */ static void pid_publish(pid_t* pid_slot, pid_t pid) { if (pid_slot == 0) return; exec_lock(); if (pid <= 0) { if (*pid_slot != PID_KILL) *pid_slot = PID_NONE; } else { pid_t prev = *pid_slot; if (prev == PID_KILL) { *pid_slot = PID_KILL; pid_kill(pid); } else { *pid_slot = pid; if (prev == PID_TERM) pid_term(pid); } } exec_unlock(); } /** * Unpublish a process reference from a slot. */ static void pid_unpublish(pid_t* pid_slot, pid_t pid) { if (pid_slot == 0) return; exec_lock(); if (*pid_slot == pid || (*pid_slot < 0 && *pid_slot != PID_KILL)) *pid_slot = PID_NONE; exec_unlock(); } /** * Waits for a process with a timeout, killing the entire process group if exceeded. * Observes process termination without reaping (via waitid WNOWAIT), leaving the process * as a zombie until the caller unpublishes the process slot and performs the final reap. * Optionally delivers input to input_fd using non-blocking writes. */ static pid_t waitpid_timeout_group(pid_t pid, int64_t start, int input_fd, const char* input, uint64_t timeout_sec, int* timed_out, int* input_error) { size_t input_len = input != 0 ? strlen(input) : 0; size_t input_pos = 0; pid_t ret; *timed_out = 0; if (input_error != 0) *input_error = 0; while (1) { /* * Poll child termination with WNOWAIT so the terminated child remains * in zombie state with its PID reserved in the kernel until unpublished. * POSIX specifies that with WNOHANG, waitid() returns 0 and sets * si_pid to 0 if the child state has not changed. */ siginfo_t info; memset(&info, 0, sizeof(info)); int wret = waitid(P_PID, (id_t)pid, &info, WEXITED | WNOHANG | WNOWAIT); if (wret == 0 && info.si_pid == pid) { if (input_fd != -1) close(input_fd); return pid; } if (wret < 0) { if (errno == EINTR) continue; int saved_errno = errno; if (input_fd != -1) close(input_fd); errno = saved_errno; return -1; } if (input_fd != -1) { while (input_pos < input_len) { ssize_t written = write(input_fd, input + input_pos, input_len - input_pos); if (written > 0) { input_pos += (size_t)written; continue; } if (written == 0) break; if (errno == EINTR) continue; if (errno == EAGAIN || errno == EWOULDBLOCK) break; if (errno == EPIPE) { close(input_fd); input_fd = -1; break; } if (input_error != 0) *input_error = errno; close(input_fd); input_fd = -1; break; } if (input_fd != -1 && input_pos == input_len) { close(input_fd); input_fd = -1; } } if (timeout_sec != 0 && os_tick_sec() - start >= timeout_sec) { *timed_out = 1; if (input_fd != -1) { close(input_fd); input_fd = -1; } if (pid_kill(pid) != 0 && errno != ESRCH) return -1; /* * Wait for the process-group leader to terminate after SIGKILL. * Use WNOWAIT so the child remains a zombie until slot unpublication. */ do { memset(&info, 0, sizeof(info)); ret = waitid(P_PID, (id_t)pid, &info, WEXITED | WNOWAIT); } while (ret < 0 && errno == EINTR); if (ret < 0) return -1; return pid; } os_usleep(100000); } } /** * Executes a script directly via its file descriptor. */ int os_script(char** argv, char** envp, const char* run_as_user, uint64_t timeout_sec, pid_t* pid_slot) { char resolved_path[PATH_MAX]; pid_t pid; int ret; int status; int timed_out; int64_t start, stop; struct os_user user; int fd = verify_executable(argv[0], resolved_path, 1); if (fd < 0) { return -1; } if (verify_shebang_interpreter(fd, resolved_path) != 0) { close(fd); return -1; } if (os_user_lookup(run_as_user, &user) != 0) { close(fd); return -1; } start = os_tick_sec(); #if defined(__linux__) && defined(PR_SET_PDEATHSIG) pid_t ppid = getpid(); #endif if (pid_spawn_begin(pid_slot) != 0) { close(fd); return -1; } pid = fork(); if (pid < 0) { os_syslog(OS_LVL_INFO, "failed to fork script=%s, errno=%s(%d)", resolved_path, strerror(errno), errno); pid_unpublish(pid_slot, 0); close(fd); return -1; } if (pid == 0) { /* child process */ /* * Create a new process group for the child. * This isolates the child from signals sent to the daemon's process group * and allows the daemon to kill this process and all its future children * (the entire group) using kill(-pid, SIGTERM). */ setpgid(0, 0); /* drop privileges first (if configured) */ os_user_drop_privileges(&user); #if defined(__linux__) && defined(PR_SET_PDEATHSIG) /* send SIGKILL to child if parent daemon dies unexpectedly */ prctl(PR_SET_PDEATHSIG, SIGKILL); /* exit if parent died before prctl to avoid becoming orphaned */ if (getppid() != ppid) { _exit(127); } #endif /* io sandboxing */ int null_fd = open("/dev/null", O_RDWR | O_CLOEXEC); if (null_fd == -1) _exit(126); /* redirect stdin/out/err to /dev/null */ if (dup2(null_fd, STDIN_FILENO) == -1 || dup2(null_fd, STDOUT_FILENO) == -1 || dup2(null_fd, STDERR_FILENO) == -1) _exit(126); /* if the fd we opened is not one of the standard ones, close it */ if (null_fd > STDERR_FILENO) close(null_fd); #if defined(CLOSE_RANGE_CLOEXEC) && defined(HAVE_CLOSE_RANGE) /* * Set all fd to be closed on exec as extra safety measure * * fallback: if it fails, we assume to be still safe, as all fds and * sockets should be already created with CLOEXEC. * * For scripts we cannot set fd as CLOEXEC. */ if (fd > 3) close_range(3, fd - 1, CLOSE_RANGE_CLOEXEC); close_range(fd + 1, ~0U, CLOSE_RANGE_CLOEXEC); #endif /* restore and unblock signals */ os_signal_restore_after_fork(); /* use the resolved path for execution */ argv[0] = resolved_path; /* * Direct Execution via File Descriptor * The kernel uses the shebang in the FD to find the interpreter. */ if (envp != NULL) { int envv_count = 0; while (envp[envv_count] != NULL) { ++envv_count; } int scrubbed_count = sizeof(envp_scrubbed) / sizeof(envp_scrubbed[0]) - 1; /* use alloca instead of malloc after fork for async-signal safety in multithreaded process */ char** envp_dynamic = alloca((size_t)(scrubbed_count + envv_count + 1) * sizeof(char*)); for (int i = 0; i < scrubbed_count; ++i) { envp_dynamic[i] = envp_scrubbed[i]; } for (int i = 0; i < envv_count; ++i) { envp_dynamic[scrubbed_count + i] = envp[i]; } envp_dynamic[scrubbed_count + envv_count] = NULL; #if HAVE_FEXECVE fexecve(fd, argv, envp_dynamic); #else /* fallback: unfortunately must use the path */ execve(resolved_path, argv, envp_dynamic); #endif } else { #if HAVE_FEXECVE fexecve(fd, argv, envp_scrubbed); #else /* fallback: unfortunately must use the path */ execve(resolved_path, argv, envp_scrubbed); #endif } _exit(127); } /* parent process */ close(fd); pid_publish(pid_slot, pid); ret = waitpid_timeout_group(pid, start, -1, 0, timeout_sec, &timed_out, 0); /* * Unpublish the PID slot before reaping the zombie process. * While the child is a zombie, the OS kernel keeps the PID reserved, * preventing any new process from reusing it. Unpublishing the slot first * guarantees that concurrent os_term()/os_kill() calls will not target * a recycled PID once waitpid() finally reaps the process. */ pid_unpublish(pid_slot, pid); if (ret != -1) { do { ret = waitpid(pid, &status, 0); } while (ret == -1 && errno == EINTR); } if (ret == -1) { os_syslog(OS_LVL_INFO, "failed to wait for script, path=%s, errno=%s(%d)", resolved_path, strerror(errno), errno); return -1; } stop = os_tick_sec(); int64_t execution_time = stop - start; if (timed_out) { os_syslog(OS_LVL_INFO, "script %s timeout after %" PRId64 " seconds", resolved_path, execution_time); return 128 + SIGALRM; } if (execution_time > 30) os_syslog(OS_LVL_WARNING, "script %s took %" PRId64 " seconds", resolved_path, execution_time); if (WIFEXITED(status)) { int exit_code = WEXITSTATUS(status); if (exit_code == 0) os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds with success", resolved_path, execution_time); else os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds with exit code %d", resolved_path, execution_time, exit_code); return exit_code; } else if (WIFSIGNALED(status)) { /* child died from a signal */ int sig = WTERMSIG(status); os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds with signal %s(%d)", resolved_path, execution_time, os_signal_name(sig), sig); return 128 + sig; } else { /* in Linux it should never happen */ os_syslog(OS_LVL_INFO, "script %s terminated in %" PRId64 " seconds for unknown reason, status=%d", resolved_path, execution_time, status); return -1; } } int os_command(const char* command, const char* run_as_user, const char* stdin_text, uint64_t timeout_sec, pid_t* pid_slot) { pid_t pid; int ret; int status; int timed_out; int input_error; int pipe_fds[2] = { -1, -1 }; int64_t start, stop; struct os_user user; /* create pipe only if we have text to send */ if (stdin_text != 0) { if (pipe_cloexec(pipe_fds) < 0) { os_syslog(OS_LVL_INFO, "failed to create input pipe, errno=%s(%d)", strerror(errno), errno); return -1; } int flags = fcntl(pipe_fds[1], F_GETFL); if (flags < 0 || fcntl(pipe_fds[1], F_SETFL, flags | O_NONBLOCK) < 0) { os_syslog(OS_LVL_INFO, "failed to make input pipe non-blocking, errno=%s(%d)", strerror(errno), errno); close(pipe_fds[0]); close(pipe_fds[1]); return -1; } } if (os_user_lookup(run_as_user, &user) != 0) { if (pipe_fds[0] != -1) { close(pipe_fds[0]); close(pipe_fds[1]); } return -1; } start = os_tick_sec(); #if defined(__linux__) && defined(PR_SET_PDEATHSIG) pid_t ppid = getpid(); #endif if (pid_spawn_begin(pid_slot) != 0) { if (pipe_fds[0] != -1) { close(pipe_fds[0]); close(pipe_fds[1]); } return -1; } pid = fork(); if (pid < 0) { os_syslog(OS_LVL_INFO, "failed to fork command=%s, errno=%s(%d)", command, strerror(errno), errno); pid_unpublish(pid_slot, 0); if (pipe_fds[0] != -1) { close(pipe_fds[0]); close(pipe_fds[1]); } return -1; } if (pid == 0) { /* child process */ /* * Create a new process group for the child. * This isolates the child from signals sent to the daemon's process group * and allows the daemon to kill this process and all its future children * (the entire group) using kill(-pid, SIGTERM). */ setpgid(0, 0); if (pipe_fds[1] != -1) close(pipe_fds[1]); /* close unused write end */ /* drop privileges first (if configured) */ os_user_drop_privileges(&user); #if defined(__linux__) && defined(PR_SET_PDEATHSIG) /* send SIGKILL to child if parent daemon dies unexpectedly */ prctl(PR_SET_PDEATHSIG, SIGKILL); /* exit if parent died before prctl to avoid becoming orphaned */ if (getppid() != ppid) { _exit(127); } #endif /* io sandboxing */ int null_fd = open("/dev/null", O_RDWR | O_CLOEXEC); if (null_fd == -1) _exit(126); /* redirect STDIN: either from pipe or /dev/null */ if (pipe_fds[0] != -1) { if (dup2(pipe_fds[0], STDIN_FILENO) == -1) _exit(126); close(pipe_fds[0]); } else { if (dup2(null_fd, STDIN_FILENO) == -1) _exit(126); } /* redirect STDOUT and STDERR to /dev/null */ if (dup2(null_fd, STDOUT_FILENO) == -1 || dup2(null_fd, STDERR_FILENO) == -1) _exit(126); /* if the fd we opened is not one of the standard ones, close it */ if (null_fd > STDERR_FILENO) close(null_fd); #if defined(CLOSE_RANGE_CLOEXEC) && defined(HAVE_CLOSE_RANGE) /* * Set all fd to be closed on exec as extra safety measure * * fallback: if it fails, we assume to be still safe, as all fds and * sockets should be already created with CLOEXEC. */ close_range(3, ~0U, CLOSE_RANGE_CLOEXEC); #endif /* restore and unblock signals */ os_signal_restore_after_fork(); char* const argv[] = { "sh", "-c", (char*)command, 0 }; execve("/bin/sh", argv, envp_scrubbed); _exit(127); } /* parent process */ if (pipe_fds[0] != -1) close(pipe_fds[0]); /* close unused read end */ pid_publish(pid_slot, pid); ret = waitpid_timeout_group(pid, start, pipe_fds[1], stdin_text, timeout_sec, &timed_out, &input_error); pipe_fds[1] = -1; /* * Unpublish the PID slot before reaping the zombie process. * While the child is a zombie, the OS kernel keeps the PID reserved, * preventing any new process from reusing it. Unpublishing the slot first * guarantees that concurrent os_term()/os_kill() calls will not target * a recycled PID once waitpid() finally reaps the process. */ pid_unpublish(pid_slot, pid); if (input_error != 0) os_syslog(OS_LVL_INFO, "failed to write stdin to command %s, errno=%s(%d)", command, strerror(input_error), input_error); if (ret != -1) { do { ret = waitpid(pid, &status, 0); } while (ret == -1 && errno == EINTR); } if (ret == -1) { os_syslog(OS_LVL_INFO, "failed to wait for command, command=%s, errno=%s(%d)", command, strerror(errno), errno); return -1; } stop = os_tick_sec(); int64_t execution_time = stop - start; if (timed_out) { os_syslog(OS_LVL_INFO, "command %s timeout after %" PRId64 " seconds", command, execution_time); return 128 + SIGALRM; } if (execution_time > 30) os_syslog(OS_LVL_WARNING, "command %s ran for %" PRId64 " seconds that is unexpectedly long", command, execution_time); if (WIFEXITED(status)) { int exit_code = WEXITSTATUS(status); if (exit_code == 0) os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds with success", command, execution_time); else os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds with exit code %d", command, execution_time, exit_code); return exit_code; } else if (WIFSIGNALED(status)) { /* child died from a signal */ int sig = WTERMSIG(status); os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds with signal %s(%d)", command, execution_time, os_signal_name(sig), sig); return 128 + sig; } else { /* in Linux it should never happen */ os_syslog(OS_LVL_INFO, "command %s terminated in %" PRId64 " seconds for unknown reason, status=%d", command, execution_time, status); return -1; } } /** * os_spawn() - Fork and execute a verified executable, capturing stdout and/or stderr. * * Spawns @argv[0] in a new process. If @stdout_read_fd is not 0, stdout is connected * to a pipe whose read end is returned in @stdout_read_fd. If @stderr_read_fd is not 0, * stderr is connected to a pipe whose read end is returned in @stderr_read_fd. * Otherwise, they are redirected to /dev/null. stdin is always redirected to /dev/null. * * The captured descriptors are raw binary file descriptors without CRLF translation. * * The child is placed in its own process group (setpgid) to isolate it from signals * sent to the daemon's process group. * * Returns the child PID on success, or -1 on failure. */ pid_t os_spawn(char** argv, int* stdout_read_fd, int* stderr_read_fd, const char* run_as_user, pid_t* pid_slot) { char resolved_path[PATH_MAX]; int out_pipe[2]; int err_pipe[2]; int has_out = (stdout_read_fd != NULL); int has_err = (stderr_read_fd != NULL); pid_t pid; struct os_user user; int fd = verify_executable(argv[0], resolved_path, 0); if (fd < 0) { return -1; } if (has_out) { if (pipe_cloexec(out_pipe) < 0) { os_syslog(OS_LVL_INFO, "failed to open output pipe, errno=%s(%d)", strerror(errno), errno); close(fd); return -1; } } if (has_err) { if (pipe_cloexec(err_pipe) < 0) { os_syslog(OS_LVL_INFO, "failed to open error pipe, errno=%s(%d)", strerror(errno), errno); if (has_out) { close(out_pipe[0]); close(out_pipe[1]); } close(fd); return -1; } } if (os_user_lookup(run_as_user, &user) != 0) { if (has_out) { close(out_pipe[0]); close(out_pipe[1]); } if (has_err) { close(err_pipe[0]); close(err_pipe[1]); } close(fd); return -1; } #if defined(__linux__) && defined(PR_SET_PDEATHSIG) pid_t ppid = getpid(); #endif if (pid_spawn_begin(pid_slot) != 0) { if (has_out) { close(out_pipe[0]); close(out_pipe[1]); } if (has_err) { close(err_pipe[0]); close(err_pipe[1]); } close(fd); return -1; } pid = fork(); if (pid < 0) { os_syslog(OS_LVL_INFO, "failed to fork path=%s, errno=%s(%d)", resolved_path, strerror(errno), errno); pid_unpublish(pid_slot, 0); if (has_out) { close(out_pipe[0]); close(out_pipe[1]); } if (has_err) { close(err_pipe[0]); close(err_pipe[1]); } close(fd); return -1; } if (pid == 0) { /* child process */ setpgid(0, 0); /* drop privileges first (if configured) */ os_user_drop_privileges(&user); #if defined(__linux__) && defined(PR_SET_PDEATHSIG) /* send SIGKILL to child if parent daemon dies unexpectedly */ prctl(PR_SET_PDEATHSIG, SIGKILL); /* exit if parent died before prctl to avoid becoming orphaned */ if (getppid() != ppid) { _exit(127); } #endif /* io sandboxing */ int null_fd = open("/dev/null", O_RDWR | O_CLOEXEC); if (null_fd < 0) _exit(126); /* stdin -> /dev/null */ if (dup2(null_fd, STDIN_FILENO) < 0) _exit(126); /* stdout */ if (has_out) { if (dup2(out_pipe[1], STDOUT_FILENO) < 0) _exit(126); } else { if (dup2(null_fd, STDOUT_FILENO) < 0) _exit(126); } /* stderr */ if (has_err) { if (dup2(err_pipe[1], STDERR_FILENO) < 0) _exit(126); } else { if (dup2(null_fd, STDERR_FILENO) < 0) _exit(126); } if (has_out) { close(out_pipe[0]); close(out_pipe[1]); } if (has_err) { close(err_pipe[0]); close(err_pipe[1]); } if (null_fd > STDERR_FILENO) close(null_fd); #if defined(CLOSE_RANGE_CLOEXEC) && defined(HAVE_CLOSE_RANGE) /* * Set all fd to be closed on exec as extra safety measure * * fallback: if it fails, we assume to be still safe, as all fds and * sockets should be already created with CLOEXEC. * * For binaries fd can be set as CLOEXEC because fexecve loads the ELF image into memory before closing descriptors. */ close_range(3, ~0U, CLOSE_RANGE_CLOEXEC); #endif /* restore and unblock signals */ os_signal_restore_after_fork(); argv[0] = resolved_path; #if HAVE_FEXECVE fexecve(fd, argv, envp_scrubbed); #else execve(resolved_path, argv, envp_scrubbed); #endif _exit(127); } /* parent */ close(fd); pid_publish(pid_slot, pid); if (has_out) { #ifdef F_SETPIPE_SZ if (fcntl(out_pipe[0], F_SETPIPE_SZ, 4096) == -1) { os_syslog(OS_LVL_INFO, "failed to set pipe size, errno=%s(%d)", strerror(errno), errno); } #endif close(out_pipe[1]); *stdout_read_fd = out_pipe[0]; } if (has_err) { #ifdef F_SETPIPE_SZ if (fcntl(err_pipe[0], F_SETPIPE_SZ, 4096) == -1) { os_syslog(OS_LVL_INFO, "failed to set pipe size, errno=%s(%d)", strerror(errno), errno); } #endif close(err_pipe[1]); *stderr_read_fd = err_pipe[0]; } return pid; } /** * Get the operating system process ID suitable for display or logging. * This function is intended for display purposes only and not as a process reference. */ uint64_t os_pid(pid_t pid) { if (pid <= 0) return 0; return (uint64_t)pid; } /** * Get the operating system process ID of a published process slot suitable for display or logging. * This function is intended for display purposes only and not as a process reference. */ uint64_t os_slot_pid(const pid_t* pid_slot) { if (pid_slot == 0) return 0; exec_lock(); uint64_t pid = os_pid(*pid_slot); exec_unlock(); return pid; } /** * Wait for the child process to terminate. */ pid_t os_wait(pid_t pid, int* status, pid_t* pid_slot) { pid_t ret; /* * If published in a slot, first wait for termination without reaping (WNOWAIT). * The child enters zombie state, holding its PID reserved in the kernel so * no newly created process can receive the same PID. We then unpublish * the slot before the final waitpid() reap, ensuring concurrent termination * requests (os_term/os_kill) never signal a recycled PID. */ if (pid_slot != 0) { siginfo_t info; int wret; do { memset(&info, 0, sizeof(info)); wret = waitid(P_PID, (id_t)pid, &info, WEXITED | WNOWAIT); } while (wret == -1 && errno == EINTR); if (wret == -1) os_syslog(OS_LVL_INFO, "failed to waitid, errno=%s(%d)", strerror(errno), errno); pid_unpublish(pid_slot, pid); } do { ret = waitpid(pid, status, 0); } while (ret == -1 && errno == EINTR); if (ret == -1) { os_syslog(OS_LVL_INFO, "failed to wait, errno=%s(%d)", strerror(errno), errno); } return ret; } /** * Release the process reference returned by os_spawn(). */ void os_dispose(pid_t pid) { (void)pid; } /** * Gracefully terminate a published process. */ int os_term(pid_t* pid_slot) { if (pid_slot == 0) { errno = EINVAL; return -1; } exec_lock(); pid_t pid = *pid_slot; if (pid == PID_SPAWN) { *pid_slot = PID_TERM; exec_unlock(); return 0; } if (pid == PID_NONE || pid == PID_TERM || pid == PID_KILL) { exec_unlock(); return 0; } int ret = pid_term(pid); exec_unlock(); return ret; } /** * Forcibly terminate a published process. */ int os_kill(pid_t* pid_slot) { if (pid_slot == 0) { errno = EINVAL; return -1; } exec_lock(); pid_t pid = *pid_slot; if (pid == PID_NONE || pid == PID_SPAWN || pid == PID_TERM) { *pid_slot = PID_KILL; exec_unlock(); return 0; } if (pid == PID_KILL) { exec_unlock(); return 0; } *pid_slot = PID_KILL; int ret = pid_kill(pid); exec_unlock(); return ret; } int os_spawn_and_wait(const char** argv) { pid_t pid = os_spawn((char**)argv, 0, 0, 0, 0); if (pid < 0) { /* LCOV_EXCL_START */ return -1; /* LCOV_EXCL_STOP */ } int status; int ret = os_wait(pid, &status, 0); os_dispose(pid); if (ret == -1) { /* LCOV_EXCL_START */ return -1; /* LCOV_EXCL_STOP */ } if (WIFEXITED(status)) { return WEXITSTATUS(status); } else if (WIFSIGNALED(status)) { return 128 + WTERMSIG(status); } return -1; } OS_FILE* os_popen(const char** argv) { int stdout_fd = -1; OS_FILE* os_file = malloc(sizeof(OS_FILE)); if (!os_file) return 0; pid_t pid = os_spawn((char**)argv, &stdout_fd, 0, 0, 0); if (pid < 0) { free(os_file); return 0; } FILE* fp = fdopen(stdout_fd, "r"); if (!fp) { /* LCOV_EXCL_START */ int saved_errno = errno; close(stdout_fd); int status; os_wait(pid, &status, 0); os_dispose(pid); errno = saved_errno; free(os_file); return 0; /* LCOV_EXCL_STOP */ } os_file->fp = fp; os_file->pid = pid; return os_file; } char* os_fgets(char* s, int size, OS_FILE* stream) { return fgets(s, size, stream->fp); } int os_pclose(OS_FILE* stream) { /* leep handles locally to free container block immediately */ FILE* fp = stream->fp; pid_t pid = stream->pid; free(stream); /* close buffered stream, releasing the underlying fd */ fclose(fp); int status = 0; int ret = os_wait(pid, &status, 0); os_dispose(pid); if (ret < 0) { return -1; } return status; } /****************************************************************************/ /* privileges */ static uid_t unpriv_uid = -1; static gid_t unpriv_gid = -1; static int uid_dropped = 0; static int gid_dropped = 0; static int groups_dropped = 0; static gid_t* privileged_groups = 0; static int privileged_group_count = 0; int os_privileges_drop(void) { /* * Detect if running as root * * Note that all calls to os_privileges_release() and os_privileges_acquire() * are no-ops before a privilege is actually dropped. */ if (getuid() == 0 || geteuid() == 0) { /* find the unprivileged user "nobody" */ struct passwd* pw = getpwnam("nobody"); if (!pw) { os_syslog(OS_LVL_INFO, "unprivileged user 'nobody' not found"); return -1; } unpriv_uid = pw->pw_uid; unpriv_gid = pw->pw_gid; /* * Save the supplementary groups before dropping them. * They must be restored by os_privileges_acquire() because * spawned user scripts may depend on them. */ privileged_group_count = getgroups(0, 0); if (privileged_group_count < 0) { os_syslog(OS_LVL_INFO, "failed to get supplementary group count, errno=%s(%d)", strerror(errno), errno); return -1; } if (privileged_group_count != 0) { privileged_groups = malloc(privileged_group_count * sizeof(gid_t)); if (!privileged_groups) { os_syslog(OS_LVL_CRITICAL, "failed to allocate supplementary group list"); return -1; } if (getgroups(privileged_group_count, privileged_groups) != privileged_group_count) { os_syslog(OS_LVL_INFO, "failed to get supplementary groups, errno=%s(%d)", strerror(errno), errno); return -1; } if (setgroups(0, 0) != 0) { os_syslog(OS_LVL_INFO, "failed to release supplementary group privileges, errno=%s(%d)", strerror(errno), errno); return -1; } groups_dropped = 1; } if (setegid(unpriv_gid) != 0) { os_syslog(OS_LVL_INFO, "failed to release group privileges, errno=%s(%d)", strerror(errno), errno); return -1; } gid_dropped = 1; if (seteuid(unpriv_uid) != 0) { os_syslog(OS_LVL_INFO, "failed to release privileges, errno=%s(%d)", strerror(errno), errno); return -1; } uid_dropped = 1; } return 0; } void os_privileges_acquire(void) { #if defined(__linux__) && defined(SYS_setresuid) && defined(SYS_setresgid) /* * On Linux with glibc/NPTL, the libc credential functions synchronize * credentials across all threads using SIGSETXID. * * Use raw syscalls instead so privilege changes remain local to the * calling thread. This applies to supplementary groups as well as UID/GID. */ /* * Restore UID first so that CAP_SETGID is available when restoring * the effective GID and supplementary groups. */ if (uid_dropped) { if (syscall(SYS_setresuid, (uid_t)-1, (uid_t)0, (uid_t)-1) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to acquire privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to acquire privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } if (gid_dropped) { if (syscall(SYS_setresgid, (gid_t)-1, (gid_t)0, (gid_t)-1) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to acquire group privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to acquire group privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } if (groups_dropped) { if (syscall(SYS_SETGROUPS, privileged_group_count, privileged_groups) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to acquire supplementary group privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to acquire supplementary group privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } #else if (uid_dropped) { if (seteuid(0) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to acquire privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to acquire privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } if (gid_dropped) { if (setegid(0) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to acquire group privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to acquire group privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } if (groups_dropped) { if (setgroups(privileged_group_count, privileged_groups) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to acquire supplementary group privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to acquire supplementary group privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } #endif } void os_privileges_release(void) { #if defined(__linux__) && defined(SYS_setresuid) && defined(SYS_setresgid) /* * Drop supplementary groups first while the calling thread still has * the privileges required to change them. */ if (groups_dropped) { if (syscall(SYS_SETGROUPS, (size_t)0, (gid_t*)0) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to release supplementary group privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to release supplementary group privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } if (gid_dropped) { if (syscall(SYS_setresgid, (gid_t)-1, (gid_t)unpriv_gid, (gid_t)-1) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to release group privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to release group privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } /* drop UID last. */ if (uid_dropped) { if (syscall(SYS_setresuid, (uid_t)-1, (uid_t)unpriv_uid, (uid_t)-1) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to release privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to release privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } #else if (groups_dropped) { if (setgroups(0, 0) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to release supplementary group privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to release supplementary group privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } if (gid_dropped) { if (setegid(unpriv_gid) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to release group privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to release group privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } /* drop UID last. */ if (uid_dropped) { if (seteuid(unpriv_uid) != 0) { if (errno == EPERM) { os_syslog(OS_LVL_INFO, "permission denied to release privileges, continuing with active privileges"); } else { os_syslog(OS_LVL_INFO, "failed to release privileges, errno=%s(%d)", strerror(errno), errno); os_abort(); } } } #endif } /****************************************************************************/ /* os */ uint64_t os_tick(void) { #if HAVE_CLOCK_GETTIME && (defined(CLOCK_UPTIME_RAW) || defined(CLOCK_MONOTONIC_RAW) || defined(CLOCK_MONOTONIC)) struct timespec tv; /* nanosecond precision with clock_gettime() */ #if defined(CLOCK_UPTIME_RAW) /* for macOS (without sleep and without NTP slewing) */ if (clock_gettime(CLOCK_UPTIME_RAW, &tv) != 0) { #elif defined(CLOCK_MONOTONIC_RAW) /* for Linux (without sleep and without NTP slewing) */ if (clock_gettime(CLOCK_MONOTONIC_RAW, &tv) != 0) { #else /* for other POSIX */ if (clock_gettime(CLOCK_MONOTONIC, &tv) != 0) { #endif /* LCOV_EXCL_START */ return 0; /* LCOV_EXCL_STOP */ } return tv.tv_sec * 1000000000ULL + tv.tv_nsec; #else /* other platforms */ struct timeval tv; /* microsecond precision with gettimeofday() */ if (gettimeofday(&tv, 0) != 0) { /* LCOV_EXCL_START */ return 0; /* LCOV_EXCL_STOP */ } return tv.tv_sec * 1000000000ULL + tv.tv_usec * 1000ULL; #endif } uint64_t os_tick_ms(void) { #if HAVE_CLOCK_GETTIME && defined(CLOCK_MONOTONIC) struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { /* LCOV_EXCL_START */ return 0; /* LCOV_EXCL_STOP */ } return ts.tv_sec * 1000ULL + ts.tv_nsec / 1000000; #else struct timeval tv; if (gettimeofday(&tv, 0) != 0) { /* LCOV_EXCL_START */ return 0; /* LCOV_EXCL_STOP */ } return tv.tv_sec * 1000ULL + tv.tv_usec / 1000; #endif } uint64_t os_tick_us(void) { return os_tick() / 1000; } uint64_t os_tick_ns(void) { return os_tick(); } uint64_t os_tick_sec(void) { #if HAVE_CLOCK_GETTIME && defined(CLOCK_MONOTONIC) struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { /* LCOV_EXCL_START */ return 0; /* LCOV_EXCL_STOP */ } return ts.tv_sec; #else struct timeval tv; if (gettimeofday(&tv, 0) != 0) { /* LCOV_EXCL_START */ return 0; /* LCOV_EXCL_STOP */ } return tv.tv_sec; #endif } int os_usleep(uint64_t usec) { while (usec > 0) { unsigned chunk = 999999; if (usec < chunk) chunk = (unsigned)usec; if (os_signal_interrupt()) { errno = EINTR; return -1; } /* * Sleep in intervals of at most 999999 microseconds because POSIX usleep() * only accepts values less than 1,000,000 microseconds (< 1 second) * and may fail with EINVAL for larger values. */ if (usleep(chunk) != 0) return -1; usec -= chunk; } return 0; } int os_randomize(void* ptr, size_t size) { int f; ssize_t ret; f = open("/dev/urandom", O_RDONLY | O_CLOEXEC); if (f == -1) { /* LCOV_EXCL_START */ return -1; /* LCOV_EXCL_STOP */ } ret = read(f, ptr, size); if (ret < 0 || (size_t)ret != size) { /* LCOV_EXCL_START */ close(f); return -1; /* LCOV_EXCL_STOP */ } if (close(f) != 0) { /* LCOV_EXCL_START */ return -1; /* LCOV_EXCL_STOP */ } return 0; } void os_clear(void) { /* ANSI codes */ printf("\033[H"); /* cursor at topleft */ printf("\033[2J"); /* clear screen */ } /* LCOV_EXCL_START */ void os_abort(void) { #if HAVE_BACKTRACE && HAVE_BACKTRACE_SYMBOLS void* stack[32]; char** messages; unsigned size; unsigned i; #endif const char* platform; const char* compiler; #if defined(__linux__) platform = ", linux"; #elif defined(__APPLE__) platform = ", macOS"; #elif defined(__FreeBSD__) platform = ", FreeBSD"; #else platform = ""; #endif #if defined(__clang__) compiler = ", clang " __clang_version__; #elif defined(__GNUC__) compiler = ", gcc " __VERSION__; #else compiler = ""; #endif os_syslog(OS_LVL_CRITICAL, "Stacktrace of " PACKAGE " v" VERSION "%s%s, %d-bit, PATH_MAX=%d", platform, compiler, (int)sizeof(void*) * 8, PATH_MAX); #if HAVE_BACKTRACE && HAVE_BACKTRACE_SYMBOLS size = backtrace(stack, 32); messages = backtrace_symbols(stack, size); for (i = 1; i < size; ++i) { const char* msg; if (messages) msg = messages[i]; else msg = ""; os_syslog(OS_LVL_CRITICAL, "[bt] %02u: %s", i, msg); if (messages) { int ret; char addr2line[1024]; char path[1024]; size_t j = 0; while (msg[j] != '(' && msg[j] != ' ' && msg[j] != 0) ++j; if (j >= sizeof(path)) j = sizeof(path) - 1; memcpy(path, msg, j); path[j] = 0; /* * Verify that the path consists only of safe characters (alphanumeric, slash, dot, dash, underscore). * This prevents arbitrary command execution via crafted executable names or directories. */ if (j > 0 && strspn(path, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/.-_") == j) { snprintf(addr2line, sizeof(addr2line), "addr2line %p -e %s", stack[i], path); ret = system(addr2line); if (WIFEXITED(ret) && WEXITSTATUS(ret) != 0) os_syslog(OS_LVL_CRITICAL, "exit:%d", WEXITSTATUS(ret)); if (WIFSIGNALED(ret)) os_syslog(OS_LVL_CRITICAL, "signal:%d", WTERMSIG(ret)); } } } #endif os_syslog(OS_LVL_CRITICAL, "Please report this error to the SnapRAID Issues:"); os_syslog(OS_LVL_CRITICAL, "https://github.com/amadvance/snapraid/issues"); abort(); } /* LCOV_EXCL_STOP */ void os_exit(void) { exit(EXIT_FAILURE); } void os_init(unsigned opt) { (void)opt; } void os_done(void) { } #endif