diff options
author | Lennart Poettering <lennart@poettering.net> | 2017-12-22 13:08:14 +0100 |
---|---|---|
committer | Sven Eden <yamakuzure@gmx.net> | 2018-05-30 07:49:44 +0200 |
commit | 45aef547896d1e91b2b2a121566320b5c3a17948 (patch) | |
tree | 4e4684c715e966e9fc6edfdc89def6c31124663d /src | |
parent | 7cfc65cbfb5fb2e06710cab0d5008a0643735d49 (diff) |
tree-wide: introduce new safe_fork() helper and port everything over
This adds a new safe_fork() wrapper around fork() and makes use of it
everywhere. The new wrapper does a couple of things we previously did
manually and separately in a safer, more correct and automatic way:
1. Optionally resets signal handlers/mask in the child
2. Sets a name on all processes we fork off right after forking off (and
the patch assigns useful names for all processes we fork off now,
following a systematic naming scheme: always enclosed in () – in order
to indicate that these are not proper, exec()ed processes, but only
forked off children, and if the process is long-running with only our
own code, without execve()'ing something else, it gets am "sd-" prefix.)
3. Optionally closes all file descriptors in the child
4. Optionally sets a PR_SET_DEATHSIG to SIGTERM in the child, in a safe
way so that the parent dying before this happens being handled
safely.
5. Optionally reopens the logs
6. Optionally connects stdin/stdout/stderr to /dev/null
7. Debug logs about the forked off processes.
Diffstat (limited to 'src')
-rw-r--r-- | src/basic/exec-util.c | 25 | ||||
-rw-r--r-- | src/basic/process-util.c | 148 | ||||
-rw-r--r-- | src/basic/process-util.h | 16 | ||||
-rw-r--r-- | src/basic/terminal-util.c | 18 | ||||
-rw-r--r-- | src/basic/time-util.c | 13 | ||||
-rw-r--r-- | src/basic/util.c | 46 | ||||
-rw-r--r-- | src/libelogind/sd-bus/bus-container.c | 16 | ||||
-rw-r--r-- | src/libelogind/sd-bus/bus-socket.c | 13 | ||||
-rw-r--r-- | src/login/inhibit.c | 15 | ||||
-rw-r--r-- | src/shared/pager.c | 47 | ||||
-rw-r--r-- | src/test/test-process-util.c | 24 |
11 files changed, 241 insertions, 140 deletions
diff --git a/src/basic/exec-util.c b/src/basic/exec-util.c index 718fbf7ee..19ac3686c 100644 --- a/src/basic/exec-util.c +++ b/src/basic/exec-util.c @@ -48,20 +48,19 @@ assert_cc(EAGAIN == EWOULDBLOCK); static int do_spawn(const char *path, char *argv[], int stdout_fd, pid_t *pid) { pid_t _pid; + int r; if (null_or_empty_path(path)) { log_debug("%s is empty (a mask).", path); return 0; } - _pid = fork(); - if (_pid < 0) - return log_error_errno(errno, "Failed to fork: %m"); - if (_pid == 0) { + r = safe_fork("(direxec)", FORK_DEATHSIG, &_pid); + if (r < 0) + return log_error_errno(r, "Failed to fork: %m"); + if (r == 0) { char *_argv[2]; - assert_se(prctl(PR_SET_PDEATHSIG, SIGTERM) == 0); - if (stdout_fd >= 0) { /* If the fd happens to be in the right place, go along with that */ if (stdout_fd != STDOUT_FILENO && @@ -107,11 +106,6 @@ static int do_execute( * If callbacks is nonnull, execution is serial. Otherwise, we default to parallel. */ - (void) reset_all_signal_handlers(); - (void) reset_signal_mask(); - - assert_se(prctl(PR_SET_PDEATHSIG, SIGTERM) == 0); - r = conf_files_list_strv(&paths, NULL, NULL, CONF_FILES_EXECUTABLE, (const char* const*) directories); if (r < 0) return r; @@ -226,11 +220,10 @@ int execute_directories( * them to finish. Optionally a timeout is applied. If a file with the same name * exists in more than one directory, the earliest one wins. */ - executor_pid = fork(); - if (executor_pid < 0) - return log_error_errno(errno, "Failed to fork: %m"); - - if (executor_pid == 0) { + r = safe_fork("(sd-executor)", FORK_RESET_SIGNALS|FORK_DEATHSIG, &executor_pid); + if (r < 0) + return log_error_errno(r, "Failed to fork: %m"); + if (r == 0) { r = do_execute(dirs, timeout, callbacks, callback_args, fd, argv); _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS); } diff --git a/src/basic/process-util.c b/src/basic/process-util.c index 2494ba6af..900e42ee8 100644 --- a/src/basic/process-util.c +++ b/src/basic/process-util.c @@ -26,6 +26,7 @@ #include <signal.h> #include <stdbool.h> #include <stdio.h> +//#include <stdio_ext.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> @@ -55,6 +56,7 @@ //#include "stat-util.h" #include "string-table.h" #include "string-util.h" +//#include "terminal-util.h" #include "user-util.h" #include "util.h" @@ -130,6 +132,8 @@ int get_process_cmdline(pid_t pid, size_t max_length, bool comm_fallback, char * return -errno; } + (void) __fsetlocking(f, FSETLOCKING_BYCALLER); + if (max_length == 1) { /* If there's only room for one byte, return the empty string */ @@ -408,6 +412,8 @@ int is_kernel_thread(pid_t pid) { return -errno; } + (void) __fsetlocking(f, FSETLOCKING_BYCALLER); + count = fread(&c, 1, 1, f); eof = feof(f); fclose(f); @@ -492,6 +498,8 @@ static int get_process_id(pid_t pid, const char *field, uid_t *uid) { return -errno; } + (void) __fsetlocking(f, FSETLOCKING_BYCALLER); + FOREACH_LINE(line, f, return -errno) { char *l; @@ -570,6 +578,8 @@ int get_process_environ(pid_t pid, char **env) { return -errno; } + (void) __fsetlocking(f, FSETLOCKING_BYCALLER); + while ((c = fgetc(f)) != EOF) { if (!GREEDY_REALLOC(outcome, allocated, sz + 5)) return -ENOMEM; @@ -818,6 +828,8 @@ int getenv_for_pid(pid_t pid, const char *field, char **_value) { return -errno; } + (void) __fsetlocking(f, FSETLOCKING_BYCALLER); + l = strlen(field); r = 0; @@ -1137,6 +1149,142 @@ int must_be_root(void) { return -EPERM; } +int safe_fork_full( + const char *name, + const int except_fds[], + size_t n_except_fds, + ForkFlags flags, + pid_t *ret_pid) { + + pid_t original_pid, pid; + sigset_t saved_ss; + bool block_signals; + int r; + + /* A wrapper around fork(), that does a couple of important initializations in addition to mere forking. Always + * returns the child's PID in *ret_pid. Returns == 0 in the child, and > 0 in the parent. */ + + original_pid = getpid_cached(); + + block_signals = flags & (FORK_RESET_SIGNALS|FORK_DEATHSIG); + + if (block_signals) { + sigset_t ss; + + /* We temporarily block all signals, so that the new child has them blocked initially. This way, we can be sure + * that SIGTERMs are not lost we might send to the child. */ + if (sigfillset(&ss) < 0) + return log_debug_errno(errno, "Failed to reset signal set: %m"); + + if (sigprocmask(SIG_SETMASK, &ss, &saved_ss) < 0) + return log_debug_errno(errno, "Failed to reset signal mask: %m"); + } + + pid = fork(); + if (pid < 0) { + r = -errno; + + if (block_signals) /* undo what we did above */ + (void) sigprocmask(SIG_SETMASK, &saved_ss, NULL); + + return log_debug_errno(r, "Failed to fork: %m"); + } + if (pid > 0) { + /* We are in the parent process */ + + if (block_signals) /* undo what we did above */ + (void) sigprocmask(SIG_SETMASK, &saved_ss, NULL); + + log_debug("Sucessfully forked off '%s' as PID " PID_FMT ".", strna(name), pid); + + if (ret_pid) + *ret_pid = pid; + + return 1; + } + + /* We are in the child process */ + + if (flags & FORK_REOPEN_LOG) { + /* Close the logs if requested, before we log anything. And make sure we reopen it if needed. */ + log_close(); + log_set_open_when_needed(true); + } + + if (name) { + r = rename_process(name); + if (r < 0) + log_debug_errno(r, "Failed to rename process, ignoring: %m"); + } + + if (flags & FORK_DEATHSIG) + if (prctl(PR_SET_PDEATHSIG, SIGTERM) < 0) { + log_debug_errno(errno, "Failed to set death signal: %m"); + _exit(EXIT_FAILURE); + } + + if (flags & FORK_RESET_SIGNALS) { + r = reset_all_signal_handlers(); + if (r < 0) { + log_debug_errno(r, "Failed to reset signal handlers: %m"); + _exit(EXIT_FAILURE); + } + + /* This implicitly undoes the signal mask stuff we did before the fork()ing above */ + r = reset_signal_mask(); + if (r < 0) { + log_debug_errno(r, "Failed to reset signal mask: %m"); + _exit(EXIT_FAILURE); + } + } else if (block_signals) { /* undo what we did above */ + if (sigprocmask(SIG_SETMASK, &saved_ss, NULL) < 0) { + log_debug_errno(errno, "Failed to restore signal mask: %m"); + _exit(EXIT_FAILURE); + } + } + + if (flags & FORK_DEATHSIG) { + /* Let's see if the parent PID is still the one we started from? If not, then the parent + * already died by the time we set PR_SET_PDEATHSIG, hence let's emulate the effect */ + + if (getppid() != original_pid) { + log_debug("Parent died early, raising SIGTERM."); + (void) raise(SIGTERM); + _exit(EXIT_FAILURE); + } + } + + if (flags & FORK_CLOSE_ALL_FDS) { + /* Close the logs here in case it got reopened above, as close_all_fds() would close them for us */ + log_close(); + + r = close_all_fds(except_fds, n_except_fds); + if (r < 0) { + log_debug_errno(r, "Failed to close all file descriptors: %m"); + _exit(EXIT_FAILURE); + } + } + + /* When we were asked to reopen the logs, do so again now */ + if (flags & FORK_REOPEN_LOG) { + log_open(); + log_set_open_when_needed(false); + } + + if (flags & FORK_NULL_STDIO) { + r = make_null_stdio(); + if (r < 0) { + log_debug_errno(r, "Failed to connect stdin/stdout to /dev/null: %m"); + _exit(EXIT_FAILURE); + } + } + + if (ret_pid) + *ret_pid = getpid_cached(); + + return 0; +} + #if 0 /// UNNEEDED by elogind static const char *const ioprio_class_table[] = { [IOPRIO_CLASS_NONE] = "none", diff --git a/src/basic/process-util.h b/src/basic/process-util.h index a84a96862..ffd4096e5 100644 --- a/src/basic/process-util.h +++ b/src/basic/process-util.h @@ -42,7 +42,7 @@ if (_pid_ == 0) { \ _r_ = ("/proc/self/" field); \ } else { \ - _r_ = alloca(strlen("/proc/") + DECIMAL_STR_MAX(pid_t) + 1 + sizeof(field)); \ + _r_ = alloca(STRLEN("/proc/") + DECIMAL_STR_MAX(pid_t) + 1 + sizeof(field)); \ sprintf((char*) _r_, "/proc/"PID_FMT"/" field, _pid_); \ } \ _r_; \ @@ -156,3 +156,17 @@ int ioprio_parse_priority(const char *s, int *ret); pid_t getpid_cached(void); int must_be_root(void); + +typedef enum ForkFlags { + FORK_RESET_SIGNALS = 1U << 0, + FORK_CLOSE_ALL_FDS = 1U << 1, + FORK_DEATHSIG = 1U << 2, + FORK_NULL_STDIO = 1U << 3, + FORK_REOPEN_LOG = 1U << 4, +} ForkFlags; + +int safe_fork_full(const char *name, const int except_fds[], size_t n_except_fds, ForkFlags flags, pid_t *ret_pid); + +static inline int safe_fork(const char *name, ForkFlags flags, pid_t *ret_pid) { + return safe_fork_full(name, NULL, 0, flags, ret_pid); +} diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index c67b34073..9a7b18ceb 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -1119,11 +1119,10 @@ int openpt_in_namespace(pid_t pid, int flags) { if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) < 0) return -errno; - child = fork(); - if (child < 0) - return -errno; - - if (child == 0) { + r = safe_fork("(sd-openpt)", FORK_RESET_SIGNALS|FORK_DEATHSIG, &child); + if (r < 0) + return r; + if (r == 0) { int master; pair[0] = safe_close(pair[0]); @@ -1170,11 +1169,10 @@ int open_terminal_in_namespace(pid_t pid, const char *name, int mode) { if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) < 0) return -errno; - child = fork(); - if (child < 0) - return -errno; - - if (child == 0) { + r = safe_fork("(sd-terminal)", FORK_RESET_SIGNALS|FORK_DEATHSIG, &child); + if (r < 0) + return r; + if (r == 0) { int master; pair[0] = safe_close(pair[0]); diff --git a/src/basic/time-util.c b/src/basic/time-util.c index c72094188..a6bff575c 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -899,8 +899,8 @@ typedef struct ParseTimestampResult { int parse_timestamp(const char *t, usec_t *usec) { char *last_space, *tz = NULL; ParseTimestampResult *shared, tmp; - int r; pid_t pid; + int r; last_space = strrchr(t, ' '); if (last_space != NULL && timezone_is_valid(last_space + 1)) @@ -913,15 +913,12 @@ int parse_timestamp(const char *t, usec_t *usec) { if (shared == MAP_FAILED) return negative_errno(); - pid = fork(); - - if (pid == -1) { - int fork_errno = errno; + r = safe_fork("(sd-timestamp)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG, &pid); + if (r < 0) { (void) munmap(shared, sizeof *shared); - return -fork_errno; + return r; } - - if (pid == 0) { + if (r == 0) { bool with_tz = true; if (setenv("TZ", tz, 1) != 0) { diff --git a/src/basic/util.c b/src/basic/util.c index c58bf5601..51b2a9d14 100644 --- a/src/basic/util.c +++ b/src/basic/util.c @@ -190,11 +190,11 @@ int prot_from_flags(int flags) { int fork_agent(pid_t *pid, const int except[], unsigned n_except, const char *path, ...) { bool stdout_is_tty, stderr_is_tty; - pid_t parent_pid, agent_pid; - sigset_t ss, saved_ss; + pid_t agent_pid; unsigned n, i; va_list ap; char **l; + int r; assert(pid); assert(path); @@ -202,45 +202,13 @@ int fork_agent(pid_t *pid, const int except[], unsigned n_except, const char *pa /* Spawns a temporary TTY agent, making sure it goes away when * we go away */ - parent_pid = getpid_cached(); - - /* First we temporarily block all signals, so that the new - * child has them blocked initially. This way, we can be sure - * that SIGTERMs are not lost we might send to the agent. */ - assert_se(sigfillset(&ss) >= 0); - assert_se(sigprocmask(SIG_SETMASK, &ss, &saved_ss) >= 0); - - agent_pid = fork(); - if (agent_pid < 0) { - assert_se(sigprocmask(SIG_SETMASK, &saved_ss, NULL) >= 0); - return -errno; - } - - if (agent_pid != 0) { - assert_se(sigprocmask(SIG_SETMASK, &saved_ss, NULL) >= 0); - *pid = agent_pid; + r = safe_fork_full("(sd-agent)", except, n_except, FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_CLOSE_ALL_FDS, &agent_pid); + if (r < 0) + return r; + if (r > 0) return 0; - } - /* In the child: - * - * Make sure the agent goes away when the parent dies */ - if (prctl(PR_SET_PDEATHSIG, SIGTERM) < 0) - _exit(EXIT_FAILURE); - - /* Make sure we actually can kill the agent, if we need to, in - * case somebody invoked us from a shell script that trapped - * SIGTERM or so... */ - (void) reset_all_signal_handlers(); - (void) reset_signal_mask(); - - /* Check whether our parent died before we were able - * to set the death signal and unblock the signals */ - if (getppid() != parent_pid) - _exit(EXIT_SUCCESS); - - /* Don't leak fds to the agent */ - close_all_fds(except, n_except); + /* In the child: */ stdout_is_tty = isatty(STDOUT_FILENO); stderr_is_tty = isatty(STDERR_FILENO); diff --git a/src/libelogind/sd-bus/bus-container.c b/src/libelogind/sd-bus/bus-container.c index 8f6d34838..a1242c567 100644 --- a/src/libelogind/sd-bus/bus-container.c +++ b/src/libelogind/sd-bus/bus-container.c @@ -62,11 +62,10 @@ int bus_container_connect_socket(sd_bus *b) { if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, pair) < 0) return -errno; - child = fork(); - if (child < 0) - return -errno; - - if (child == 0) { + r = safe_fork("(sd-buscntr)", FORK_RESET_SIGNALS|FORK_DEATHSIG, &child); + if (r < 0) + return r; + if (r == 0) { pid_t grandchild; pair[0] = safe_close(pair[0]); @@ -82,11 +81,10 @@ int bus_container_connect_socket(sd_bus *b) { * comes from a process from within the container, and * not outside of it */ - grandchild = fork(); - if (grandchild < 0) + r = safe_fork("(sd-buscntr2)", FORK_RESET_SIGNALS|FORK_DEATHSIG, &grandchild); + if (r < 0) _exit(EXIT_FAILURE); - - if (grandchild == 0) { + if (r == 0) { r = connect(b->input_fd, &b->sockaddr.sa, b->sockaddr_size); if (r < 0) { diff --git a/src/libelogind/sd-bus/bus-socket.c b/src/libelogind/sd-bus/bus-socket.c index d72cb616e..013bdb05a 100644 --- a/src/libelogind/sd-bus/bus-socket.c +++ b/src/libelogind/sd-bus/bus-socket.c @@ -715,19 +715,14 @@ int bus_socket_exec(sd_bus *b) { if (r < 0) return -errno; - pid = fork(); - if (pid < 0) { + r = safe_fork_full("(sd-busexec)", s+1, 1, FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS, &pid); + if (r < 0) { safe_close_pair(s); - return -errno; + return r; } - if (pid == 0) { + if (r == 0) { /* Child */ - (void) reset_all_signal_handlers(); - (void) reset_signal_mask(); - - close_all_fds(s+1, 1); - assert_se(dup3(s[1], STDIN_FILENO, 0) == STDIN_FILENO); assert_se(dup3(s[1], STDOUT_FILENO, 0) == STDOUT_FILENO); diff --git a/src/login/inhibit.c b/src/login/inhibit.c index 7c48e7cee..3ea5124b3 100644 --- a/src/login/inhibit.c +++ b/src/login/inhibit.c @@ -270,20 +270,13 @@ int main(int argc, char *argv[]) { return EXIT_FAILURE; } - pid = fork(); - if (pid < 0) { - log_error_errno(errno, "Failed to fork: %m"); + r = safe_fork("(inhibit)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_CLOSE_ALL_FDS, &pid); + if (r < 0) { + log_error_errno(r, "Failed to fork: %m"); return EXIT_FAILURE; } - - if (pid == 0) { + if (r == 0) { /* Child */ - - (void) reset_all_signal_handlers(); - (void) reset_signal_mask(); - - close_all_fds(NULL, 0); - execvp(argv[optind], argv + optind); log_error_errno(errno, "Failed to execute %s: %m", argv[optind]); _exit(EXIT_FAILURE); diff --git a/src/shared/pager.c b/src/shared/pager.c index cdf6609a8..1f0d0b887 100644 --- a/src/shared/pager.c +++ b/src/shared/pager.c @@ -62,7 +62,7 @@ static bool stderr_redirected = false; int pager_open(bool no_pager, bool jump_to_end) { _cleanup_close_pair_ int fd[2] = { -1, -1 }; const char *pager; - pid_t parent_pid; + int r; if (no_pager) return 0; @@ -89,18 +89,13 @@ int pager_open(bool no_pager, bool jump_to_end) { if (pipe2(fd, O_CLOEXEC) < 0) return log_error_errno(errno, "Failed to create pager pipe: %m"); - parent_pid = getpid_cached(); - - pager_pid = fork(); - if (pager_pid < 0) - return log_error_errno(errno, "Failed to fork pager: %m"); - - /* In the child start the pager */ - if (pager_pid == 0) { + r = safe_fork("(pager)", FORK_RESET_SIGNALS|FORK_DEATHSIG, &pager_pid); + if (r < 0) + return log_error_errno(r, "Failed to fork pager: %m"); + if (r == 0) { const char* less_opts, *less_charset; - (void) reset_all_signal_handlers(); - (void) reset_signal_mask(); + /* In the child start the pager */ (void) dup2(fd[0], STDIN_FILENO); safe_close_pair(fd); @@ -124,15 +119,6 @@ int pager_open(bool no_pager, bool jump_to_end) { setenv("LESSCHARSET", less_charset, 1) < 0) _exit(EXIT_FAILURE); - /* Make sure the pager goes away when the parent dies */ - if (prctl(PR_SET_PDEATHSIG, SIGTERM) < 0) - _exit(EXIT_FAILURE); - - /* Check whether our parent died before we were able - * to set the death signal */ - if (getppid() != parent_pid) - _exit(EXIT_SUCCESS); - if (pager) { execlp(pager, pager, NULL); execl("/bin/sh", "sh", "-c", pager, NULL); @@ -223,24 +209,11 @@ int show_man_page(const char *desc, bool null_stdio) { } else args[1] = desc; - pid = fork(); - if (pid < 0) - return log_error_errno(errno, "Failed to fork: %m"); - - if (pid == 0) { + r = safe_fork("(man)", FORK_RESET_SIGNALS|FORK_DEATHSIG|(null_stdio ? FORK_NULL_STDIO : 0), &pid); + if (r < 0) + return log_error_errno(r, "Failed to fork: %m"); + if (r == 0) { /* Child */ - - (void) reset_all_signal_handlers(); - (void) reset_signal_mask(); - - if (null_stdio) { - r = make_null_stdio(); - if (r < 0) { - log_error_errno(r, "Failed to kill stdio: %m"); - _exit(EXIT_FAILURE); - } - } - execvp(args[0], (char**) args); log_error_errno(errno, "Failed to execute man: %m"); _exit(EXIT_FAILURE); diff --git a/src/test/test-process-util.c b/src/test/test-process-util.c index 0cb9a82d3..375001dbb 100644 --- a/src/test/test-process-util.c +++ b/src/test/test-process-util.c @@ -38,6 +38,7 @@ #include "macro.h" #include "parse-util.h" #include "process-util.h" +//#include "signal-util.h" #include "stdio-util.h" #include "string-util.h" #include "terminal-util.h" @@ -507,6 +508,28 @@ static void test_getpid_measure(void) { log_info("getpid_cached(): %llu/s\n", (unsigned long long) (MEASURE_ITERATIONS*USEC_PER_SEC/q)); } +static void test_safe_fork(void) { + siginfo_t status; + pid_t pid; + int r; + + BLOCK_SIGNALS(SIGCHLD); + + r = safe_fork("(test-child)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_NULL_STDIO|FORK_REOPEN_LOG, &pid); + assert_se(r >= 0); + + if (r == 0) { + /* child */ + usleep(100 * USEC_PER_MSEC); + + _exit(88); + } + + assert_se(wait_for_terminate(pid, &status) >= 0); + assert_se(status.si_code == CLD_EXITED); + assert_se(status.si_status == 88); +} + int main(int argc, char *argv[]) { log_set_max_level(LOG_DEBUG); @@ -537,6 +560,7 @@ int main(int argc, char *argv[]) { #endif // 0 test_getpid_cached(); test_getpid_measure(); + test_safe_fork(); return 0; } |