mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-25 18:40:19 -04:00
Secure external tools execution
This commit is contained in:
parent
0f37db8d20
commit
06b19808c1
9 changed files with 571 additions and 80 deletions
|
|
@ -7,6 +7,31 @@
|
|||
|
||||
#include "support.h"
|
||||
|
||||
/****************************************************************************/
|
||||
/* signal */
|
||||
|
||||
volatile int global_interrupt = 0;
|
||||
|
||||
int os_global_interrupt(void)
|
||||
{
|
||||
return global_interrupt;
|
||||
}
|
||||
|
||||
static void signal_handler(int signum)
|
||||
{
|
||||
/* report the request of interruption with the signal received */
|
||||
global_interrupt = signum;
|
||||
}
|
||||
|
||||
void os_signal_init(void)
|
||||
{
|
||||
signal(SIGTERM, signal_handler);
|
||||
signal(SIGINT, signal_handler);
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
/* os */
|
||||
|
||||
/**
|
||||
* Exit codes.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@
|
|||
#include "stream.h"
|
||||
#include "support.h"
|
||||
|
||||
volatile int global_interrupt = 0;
|
||||
|
||||
#define STREAM_MAX 8
|
||||
#define BUFFER_MAX 64
|
||||
#define STR_MAX 128
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@
|
|||
|
||||
#include "support.h"
|
||||
|
||||
volatile int global_interrupt = 0;
|
||||
|
||||
/****************************************************************************/
|
||||
/* random */
|
||||
|
||||
|
|
|
|||
|
|
@ -257,6 +257,22 @@
|
|||
*/
|
||||
void os_default_conf(char* dst, size_t dst_size, const char* argv0);
|
||||
|
||||
/**
|
||||
* Initialize signal handling.
|
||||
*/
|
||||
void os_signal_init();
|
||||
|
||||
/**
|
||||
* Restore signal handlers after fork in child process.
|
||||
* This resets signals to default handling.
|
||||
*/
|
||||
void os_signal_restore_after_fork(void);
|
||||
|
||||
/**
|
||||
* Global variable to identify if Ctrl+C is pressed.
|
||||
*/
|
||||
int os_global_interrupt(void);
|
||||
|
||||
/**
|
||||
* Get the os_tick counter value.
|
||||
*
|
||||
|
|
@ -290,11 +306,6 @@ void os_abort(void) __noreturn;
|
|||
*/
|
||||
void os_clear(void);
|
||||
|
||||
/**
|
||||
* Global variable to identify if Ctrl+C is pressed.
|
||||
*/
|
||||
extern volatile int global_interrupt;
|
||||
|
||||
/****************************************************************************/
|
||||
/* app */
|
||||
|
||||
|
|
|
|||
|
|
@ -887,35 +887,6 @@ int parse_option_size(const char* arg, uint64_t* out_size)
|
|||
return 0;
|
||||
}
|
||||
|
||||
volatile int global_interrupt = 0;
|
||||
|
||||
/* LCOV_EXCL_START */
|
||||
void signal_handler(int signum)
|
||||
{
|
||||
/* report the request of interruption with the signal received */
|
||||
global_interrupt = signum;
|
||||
}
|
||||
/* LCOV_EXCL_STOP */
|
||||
|
||||
void signal_init(void)
|
||||
{
|
||||
#if HAVE_SIGACTION
|
||||
struct sigaction sa;
|
||||
|
||||
sa.sa_handler = signal_handler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
|
||||
/* use the SA_RESTART to automatically restart interrupted system calls */
|
||||
sa.sa_flags = SA_RESTART;
|
||||
|
||||
sigaction(SIGHUP, &sa, 0);
|
||||
sigaction(SIGTERM, &sa, 0);
|
||||
sigaction(SIGINT, &sa, 0);
|
||||
#else
|
||||
signal(SIGINT, signal_handler);
|
||||
#endif
|
||||
}
|
||||
|
||||
#define OPERATION_DIFF 0
|
||||
#define OPERATION_SYNC 1
|
||||
#define OPERATION_CHECK 2
|
||||
|
|
@ -1849,7 +1820,7 @@ int snapraid_main(int argc, char* argv[])
|
|||
memory();
|
||||
|
||||
/* intercept signals while operating */
|
||||
signal_init();
|
||||
os_signal_init();
|
||||
|
||||
#if HAVE_CHECKER
|
||||
/* run a test command if required */
|
||||
|
|
@ -1900,14 +1871,14 @@ int snapraid_main(int argc, char* argv[])
|
|||
memory();
|
||||
|
||||
/* intercept signals while operating */
|
||||
signal_init();
|
||||
os_signal_init();
|
||||
|
||||
ret = state_dry(&state, blockstart, blockcount);
|
||||
} else if (operation == OPERATION_REHASH) {
|
||||
state_read(&state);
|
||||
|
||||
/* intercept signals while operating */
|
||||
signal_init();
|
||||
os_signal_init();
|
||||
|
||||
state_rehash(&state);
|
||||
|
||||
|
|
@ -1923,7 +1894,7 @@ int snapraid_main(int argc, char* argv[])
|
|||
memory();
|
||||
|
||||
/* intercept signals while operating */
|
||||
signal_init();
|
||||
os_signal_init();
|
||||
|
||||
state_snapshot_read(&state);
|
||||
|
||||
|
|
@ -1932,7 +1903,7 @@ int snapraid_main(int argc, char* argv[])
|
|||
state_read(&state);
|
||||
|
||||
/* intercept signals while operating */
|
||||
signal_init();
|
||||
os_signal_init();
|
||||
|
||||
state_write(&state);
|
||||
|
||||
|
|
@ -1951,7 +1922,7 @@ int snapraid_main(int argc, char* argv[])
|
|||
state_touch(&state);
|
||||
|
||||
/* intercept signals while operating */
|
||||
signal_init();
|
||||
os_signal_init();
|
||||
|
||||
state_write(&state);
|
||||
} else if (operation == OPERATION_SPINUP) {
|
||||
|
|
@ -2039,7 +2010,7 @@ int snapraid_main(int argc, char* argv[])
|
|||
memory();
|
||||
|
||||
/* intercept signals while operating */
|
||||
signal_init();
|
||||
os_signal_init();
|
||||
|
||||
state_snapshot_write(&state, &filterlist_disk);
|
||||
|
||||
|
|
@ -2086,19 +2057,19 @@ int snapraid_main(int argc, char* argv[])
|
|||
exit(EXIT_FAILURE);
|
||||
/* LCOV_EXCL_STOP */
|
||||
}
|
||||
if (global_interrupt) {
|
||||
if (os_global_interrupt()) {
|
||||
/* LCOV_EXCL_START */
|
||||
#ifdef _WIN32
|
||||
exit(STATUS_CONTROL_C_EXIT);
|
||||
#else
|
||||
/* restore default handler */
|
||||
signal(global_interrupt, SIG_DFL);
|
||||
signal(os_global_interrupt(), SIG_DFL);
|
||||
|
||||
/* raise the signal again*/
|
||||
raise(global_interrupt);
|
||||
raise(os_global_interrupt());
|
||||
|
||||
/* if raise() didn't terminate */
|
||||
_exit(128 + global_interrupt);
|
||||
_exit(128 + os_global_interrupt());
|
||||
#endif
|
||||
/* LCOV_EXCL_STOP */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4966,7 +4966,7 @@ int state_progress_begin(struct snapraid_state* state, block_off_t blockstart, b
|
|||
return -1;
|
||||
|
||||
/* stop if requested */
|
||||
if (global_interrupt) {
|
||||
if (os_global_interrupt()) {
|
||||
/* LCOV_EXCL_START */
|
||||
if (!state->opt.gui) {
|
||||
msg_status("Not starting due to interruption\n");
|
||||
|
|
@ -5487,7 +5487,7 @@ int state_progress(struct snapraid_state* state, struct snapraid_io* io, block_o
|
|||
}
|
||||
|
||||
/* stop if requested */
|
||||
if (global_interrupt) {
|
||||
if (os_global_interrupt()) {
|
||||
/* LCOV_EXCL_START */
|
||||
if (!state->opt.gui) {
|
||||
msg_bar("\n");
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ void state_thermal_cooldown(struct snapraid_state* state)
|
|||
sleep_time -= 30;
|
||||
}
|
||||
|
||||
if (!global_interrupt) { /* don't wake-up if we are interrupting */
|
||||
if (!os_global_interrupt()) { /* don't wake-up if we are interrupting */
|
||||
log_tag("thermal:spinup\n");
|
||||
|
||||
/* spinup */
|
||||
|
|
|
|||
523
cmdline/unix.c
523
cmdline/unix.c
|
|
@ -21,10 +21,255 @@ int eaccess(const char* pathname, int mode)
|
|||
}
|
||||
#endif
|
||||
|
||||
/****************************************************************************/
|
||||
/* signal */
|
||||
|
||||
volatile int global_interrupt = 0;
|
||||
|
||||
int os_global_interrupt(void)
|
||||
{
|
||||
return global_interrupt;
|
||||
}
|
||||
|
||||
/* LCOV_EXCL_START */
|
||||
static void signal_handler(int signum)
|
||||
{
|
||||
/* report the request of interruption with the signal received */
|
||||
global_interrupt = signum;
|
||||
}
|
||||
/* LCOV_EXCL_STOP */
|
||||
|
||||
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)
|
||||
{
|
||||
struct sigaction sa;
|
||||
|
||||
sa.sa_handler = signal_handler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
|
||||
/* use the SA_RESTART to automatically restart interrupted system calls */
|
||||
sa.sa_flags = SA_RESTART;
|
||||
|
||||
sigaction(SIGHUP, &sa, 0);
|
||||
sigaction(SIGTERM, &sa, 0);
|
||||
sigaction(SIGINT, &sa, 0);
|
||||
|
||||
sa.sa_handler = SIG_IGN; /* ignore the signal */
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = 0;
|
||||
sigaction(SIGPIPE, &sa, 0);
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
/* exec */
|
||||
|
||||
#if HAVE_LINUX_DEVICE
|
||||
/*
|
||||
* 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.
|
||||
* - The file must not have more than one hard link, to prevent an attacker
|
||||
* from linking a controlled file into a trusted directory.
|
||||
* - On systems with fexecve(2) support, the returned fd is opened without
|
||||
* O_CLOEXEC so it can be passed directly to fexecve(2). On other systems
|
||||
* O_CLOEXEC is set and execve(2) must be used with @resolved_path.
|
||||
*
|
||||
* @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 log_error(...).
|
||||
*/
|
||||
static int verify_executable(const char* exec_path, char* resolved_path)
|
||||
{
|
||||
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] != '/') {
|
||||
log_error(EINVAL, "Path %s must be absolute", exec_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* resolve the path to prevent symlink attacks */
|
||||
if (!realpath(exec_path, resolved_path)) {
|
||||
log_error(errno, "Failed to resolve %s. %s.", exec_path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* last_slash = strrchr(resolved_path, '/');
|
||||
if (last_slash == 0) {
|
||||
log_error(EINVAL, "Relative execution of %s not allowed", resolved_path);
|
||||
return -1;
|
||||
}
|
||||
if (last_slash == resolved_path) {
|
||||
log_error(EINVAL, "Root dir execution of %s not allowed", resolved_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* exec_name = last_slash + 1;
|
||||
if (exec_name[0] == 0) {
|
||||
log_error(EINVAL, "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) {
|
||||
log_error(errno, "Failed to open directory %s. %s.", dir_path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fstat(dir_fd, &st) != 0) {
|
||||
log_error(errno, "Failed to stat directory %s. %s.", dir_path, strerror(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) {
|
||||
log_error(EINVAL, "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) {
|
||||
log_error(EINVAL, "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) {
|
||||
log_error(EINVAL, "Directory %s must be not world-writable", dir_path);
|
||||
close(dir_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Open the executable
|
||||
* O_NOFOLLOW prevents following symlinks to mitigate redirection attacks
|
||||
*/
|
||||
int fd = openat(dir_fd, exec_name, O_RDONLY | O_NOFOLLOW
|
||||
#if !HAVE_FEXECVE
|
||||
| O_CLOEXEC /* with fexecve cannot use O_CLOEXEC (Close on Exec) */
|
||||
#endif
|
||||
);
|
||||
if (fd < 0) {
|
||||
log_error(errno, "Failed to open %s. %s.", resolved_path, strerror(errno));
|
||||
close(dir_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
close(dir_fd);
|
||||
|
||||
/* get the file handle (TOCTOU Protection) */
|
||||
if (fstat(fd, &st) == -1) {
|
||||
log_error(errno, "Failed to stat %s. %s.", resolved_path, strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ensure it's a regular file */
|
||||
if (!S_ISREG(st.st_mode)) {
|
||||
log_error(EINVAL, "File %s is not a regular file", resolved_path);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ensure it has execute permissions */
|
||||
if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
|
||||
log_error(EINVAL, "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) {
|
||||
log_error(EINVAL, "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) {
|
||||
log_error(EINVAL, "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) {
|
||||
log_error(EINVAL, "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)) {
|
||||
log_error(EINVAL, "File %s has setuid/setgid bits set", resolved_path);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* verify the file has not been hardlinked multiple times */
|
||||
if (st.st_nlink > 1) {
|
||||
log_error(EINVAL, "File %s has multiple hard links", resolved_path);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a string for shell metacharacters.
|
||||
* Returns -1 if dangerous characters are detected, 0 otherwise.
|
||||
|
|
@ -65,7 +310,6 @@ static int validate_exec_input(const char* str)
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Scrubbed environment
|
||||
* Only provide the bare essentials.
|
||||
|
|
@ -82,40 +326,264 @@ static char* const envp_scrubbed[] = {
|
|||
NULL
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a command with arguments.
|
||||
* @param args A NULL-terminated array of strings. args[0] is the executable.
|
||||
* @return The exit status of the command, or -1 on fork/wait failure.
|
||||
*/
|
||||
static int systemv(const char* args[])
|
||||
static int pipe_cloexec(int pipefd[2])
|
||||
{
|
||||
pid_t pid = fork();
|
||||
|
||||
if (pid < 0) {
|
||||
/* LCOV_EXCL_START */
|
||||
#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
|
||||
}
|
||||
|
||||
/*
|
||||
* and execute a verified executable, capturing stdout.
|
||||
*
|
||||
* Spawns @argv[0] in a new process with stdout connected to a pipe whose
|
||||
* read end is returned in @stdout_fd. stdin and stderr are redirected to
|
||||
* /dev/null. Use this when the child's error output needs to be read and
|
||||
* processed by the daemon.
|
||||
*
|
||||
* The child is placed in its own process group (setpgid) to isolate it
|
||||
* from signals sent to the daemon's process group. The daemon can terminate
|
||||
* the child and all its descendants with kill(-pid, SIGTERM).
|
||||
*
|
||||
* The pipe is created with O_CLOEXEC on both ends. The write end is closed
|
||||
* in the parent after fork. The read end's buffer is reduced to 4096 bytes
|
||||
* to improve read responsiveness on low-volume output.
|
||||
*
|
||||
* @argv NULL-terminated argument vector. argv[0] must be the absolute
|
||||
* path to the executable.
|
||||
* @stdout_fd On success, set to the read end of the stdout pipe. The caller
|
||||
* is responsible for closing it when done.
|
||||
*
|
||||
* Returns the child PID on success, or -1 on failure.
|
||||
*/
|
||||
pid_t os_spawn_stdout(const char** argv, int* stdout_fd)
|
||||
{
|
||||
char resolved_path[PATH_MAX];
|
||||
int out_pipe[2];
|
||||
pid_t pid;
|
||||
|
||||
int fd = verify_executable(argv[0], resolved_path);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pipe_cloexec(out_pipe) < 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
close(out_pipe[0]);
|
||||
close(out_pipe[1]);
|
||||
close(fd);
|
||||
return -1;
|
||||
/* LCOV_EXCL_STOP */
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
/* io sandboxing */
|
||||
int null_fd = open("/dev/null", O_RDWR | O_CLOEXEC);
|
||||
if (null_fd < 0)
|
||||
_exit(126);
|
||||
|
||||
/* stdin -> /dev/null */
|
||||
/* stdout -> pipe */
|
||||
/* stderr -> /dev/null */
|
||||
if (dup2(null_fd, STDIN_FILENO) < 0
|
||||
|| dup2(out_pipe[1], STDOUT_FILENO) < 0
|
||||
|| dup2(null_fd, STDERR_FILENO) < 0)
|
||||
_exit(126);
|
||||
|
||||
close(out_pipe[0]);
|
||||
close(out_pipe[1]);
|
||||
|
||||
/* 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 */
|
||||
close_range(3, ~0U, CLOSE_RANGE_CLOEXEC);
|
||||
/*
|
||||
* 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, fd - 1, CLOSE_RANGE_CLOEXEC);
|
||||
close_range(fd + 1, ~0U, CLOSE_RANGE_CLOEXEC);
|
||||
#endif
|
||||
|
||||
/* child process */
|
||||
execve(args[0], (char* const*)args, envp_scrubbed);
|
||||
_exit(EXIT_FAILURE);
|
||||
/* 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 HAVE_FEXECVE
|
||||
fexecve(fd, (char**)argv, envp_scrubbed);
|
||||
#else
|
||||
/* fallback: unfortunately must use the path */
|
||||
execve(resolved_path, (char**)argv, envp_scrubbed);
|
||||
#endif
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
/* parent process */
|
||||
int status;
|
||||
pid_t ret;
|
||||
/* parent */
|
||||
close(fd);
|
||||
|
||||
/* set the pipe buffer to the minimum to improve responsiveness */
|
||||
if (fcntl(out_pipe[0], F_SETPIPE_SZ, 4096) == -1) {
|
||||
log_error(errno, "Failed to set pipe size. %s.", strerror(errno));
|
||||
/* log non-fatal error or ignore */
|
||||
}
|
||||
|
||||
close(out_pipe[1]);
|
||||
|
||||
*stdout_fd = out_pipe[0];
|
||||
return pid;
|
||||
}
|
||||
|
||||
int os_wait(pid_t pid, int* status)
|
||||
{
|
||||
int ret;
|
||||
|
||||
do {
|
||||
ret = waitpid(pid, &status, 0);
|
||||
ret = waitpid(pid, status, 0);
|
||||
} while (ret == -1 && errno == EINTR);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Fork and execute a verified executable, discarding all I/O.
|
||||
*
|
||||
* Spawns @argv[0] in a new process with stdin, stdout and stderr all
|
||||
* redirected to /dev/null. Use this for fire-and-forget tasks where the
|
||||
* child's output is not needed.
|
||||
*
|
||||
* The child is placed in its own process group (setpgid) to isolate it
|
||||
* from signals sent to the daemon's process group.
|
||||
*
|
||||
* @argv NULL-terminated argument vector. argv[0] must be the absolute path
|
||||
* to the executable.
|
||||
*
|
||||
* Returns the child execit status on success, or -1 on failure.
|
||||
*/
|
||||
int os_spawn_and_wait(const char** argv)
|
||||
{
|
||||
char resolved_path[PATH_MAX];
|
||||
pid_t pid;
|
||||
|
||||
int fd = verify_executable(argv[0], resolved_path);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
log_error(errno, "Fork failed. %s.", strerror(errno));
|
||||
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);
|
||||
|
||||
/* io sandboxing */
|
||||
int null_fd = open("/dev/null", O_RDWR | O_CLOEXEC);
|
||||
if (null_fd < 0)
|
||||
_exit(126);
|
||||
|
||||
/* stdin -> /dev/null */
|
||||
/* stdout -> /dev/null */
|
||||
/* stderr -> /dev/null */
|
||||
if (dup2(null_fd, STDIN_FILENO) < 0
|
||||
|| dup2(null_fd, STDOUT_FILENO) < 0
|
||||
|| dup2(null_fd, STDERR_FILENO) < 0)
|
||||
_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)
|
||||
/*
|
||||
* Mark all remaining open fds (>= 3) as close-on-exec as an
|
||||
* extra safety measure against leaking inherited fds into the
|
||||
* child executable.
|
||||
* Note: if fd == 3, the first range (3, fd-1) = (3, 2) is
|
||||
* intentionally invalid and will fail harmlessly; fd itself
|
||||
* is preserved for fexecve.
|
||||
*/
|
||||
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 HAVE_FEXECVE
|
||||
fexecve(fd, (char**)argv, envp_scrubbed);
|
||||
#else
|
||||
/* fallback: unfortunately must use the path */
|
||||
execve(resolved_path, (char**)argv, envp_scrubbed);
|
||||
#endif
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
/* parent */
|
||||
close(fd);
|
||||
|
||||
int status;
|
||||
int ret = os_wait(pid, &status);
|
||||
if (ret == -1) {
|
||||
/* LCOV_EXCL_START */
|
||||
log_error(errno, "Wait %s failed. %s.", resolved_path, strerror(errno));
|
||||
return -1;
|
||||
/* LCOV_EXCL_STOP */
|
||||
}
|
||||
|
|
@ -123,7 +591,7 @@ static int systemv(const char* args[])
|
|||
if (WIFEXITED(status)) {
|
||||
return WEXITSTATUS(status);
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
return -WTERMSIG(status);
|
||||
return 128 + WTERMSIG(status);
|
||||
}
|
||||
|
||||
return -1;
|
||||
|
|
@ -2282,7 +2750,7 @@ static int fssnapshot_bcachefs_create(const struct fssnapshot_struct* fss, const
|
|||
0
|
||||
};
|
||||
|
||||
ret = systemv(argv);
|
||||
ret = os_spawn_and_wait(argv);
|
||||
if (ret != 0) {
|
||||
/* LCOV_EXCL_START */
|
||||
return -1;
|
||||
|
|
@ -2321,7 +2789,7 @@ static int fssnapshot_zfs_create(const struct fssnapshot_struct* fss, const char
|
|||
0
|
||||
};
|
||||
|
||||
ret = systemv(argv);
|
||||
ret = os_spawn_and_wait(argv);
|
||||
if (ret != 0) {
|
||||
/* LCOV_EXCL_START */
|
||||
return -1;
|
||||
|
|
@ -2411,7 +2879,7 @@ static int fssnapshot_bcachefs_delete(const struct fssnapshot_struct* fss, const
|
|||
0,
|
||||
};
|
||||
|
||||
ret = systemv(argv);
|
||||
ret = os_spawn_and_wait(argv);
|
||||
if (ret != 0) {
|
||||
/* LCOV_EXCL_START */
|
||||
return -1;
|
||||
|
|
@ -2450,7 +2918,7 @@ static int fssnapshot_zfs_delete(const struct fssnapshot_struct* fss, const char
|
|||
0
|
||||
};
|
||||
|
||||
ret = systemv(argv);
|
||||
ret = os_spawn_and_wait(argv);
|
||||
if (ret != 0) {
|
||||
/* LCOV_EXCL_START */
|
||||
return -1;
|
||||
|
|
@ -2551,8 +3019,7 @@ static int fssnapshot_zfs_rename(const struct fssnapshot_struct* fss, const char
|
|||
0
|
||||
};
|
||||
|
||||
|
||||
ret = systemv(argv);
|
||||
ret = os_spawn_and_wait(argv);
|
||||
if (ret != 0) {
|
||||
/* LCOV_EXCL_START */
|
||||
return -1;
|
||||
|
|
|
|||
21
configure.ac
21
configure.ac
|
|
@ -60,6 +60,26 @@ AC_CHECK_HEADERS([pthread.h math.h])
|
|||
AC_CHECK_HEADERS([sys/file.h sys/ioctl.h sys/sysmacros.h sys/mkdev.h sys/mount.h])
|
||||
AC_CHECK_HEADERS([linux/fiemap.h linux/fs.h linux/btrfs.h mach/mach_time.h execinfo.h])
|
||||
|
||||
# Check for the close_range(...,CLOSE_RANGE_CLOEXEC)
|
||||
AC_CHECK_HEADERS([linux/close_range.h])
|
||||
AC_CHECK_FUNCS([close_range])
|
||||
AC_MSG_CHECKING([for CLOSE_RANGE_CLOEXEC])
|
||||
AC_COMPILE_IFELSE([
|
||||
AC_LANG_PROGRAM([[
|
||||
#ifdef HAVE_LINUX_CLOSE_RANGE_H
|
||||
#include <linux/close_range.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
]], [[
|
||||
unsigned int flags = CLOSE_RANGE_CLOEXEC;
|
||||
]])
|
||||
], [
|
||||
AC_MSG_RESULT([yes])
|
||||
AC_DEFINE([HAVE_CLOSE_RANGE_CLOEXEC], [1], [Define if CLOSE_RANGE_CLOEXEC is available])
|
||||
], [
|
||||
AC_MSG_RESULT([no])
|
||||
])
|
||||
|
||||
dnl Checks for typedefs, structures, and compiler characteristics.
|
||||
AC_C_CONST
|
||||
AC_C_INLINE
|
||||
|
|
@ -99,6 +119,7 @@ AC_CHECK_FUNCS([mach_absolute_time])
|
|||
AC_CHECK_FUNCS([backtrace backtrace_symbols])
|
||||
AC_SEARCH_LIBS([clock_gettime], [rt])
|
||||
AC_CHECK_FUNCS([clock_gettime])
|
||||
AC_CHECK_FUNCS([fexecve pipe2])
|
||||
AC_SEARCH_LIBS([exp], [m])
|
||||
AC_CHECK_FUNCS([eaccess faccessat])
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue