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.
This commit is contained in:
Lennart Poettering
2017-12-25 11:48:21 +01:00
parent d8caff6db6
commit 4c253ed1ca
42 changed files with 448 additions and 507 deletions
+5 -16
View File
@@ -61,29 +61,18 @@ finish:
}
int asynchronous_sync(void) {
pid_t pid;
int r;
/* This forks off an invocation of fork() as a child process, in order to initiate synchronization to
* disk. Note that we implement this as helper process rather than thread as we don't want the sync() to hang our
* original process ever, and a thread would do that as the process can't exit with threads hanging in blocking
* syscalls. */
log_debug("Spawning new process for sync");
pid = fork();
if (pid < 0)
return -errno;
if (pid == 0) {
r = safe_fork("(sd-sync)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS, NULL);
if (r < 0)
return r;
if (r == 0) {
/* Child process */
(void) rename_process("(sd-sync)");
(void) reset_all_signal_handlers();
(void) reset_signal_mask();
(void) close_all_fds(NULL, 0);
(void) sync();
_exit(EXIT_SUCCESS);
}
+5 -9
View File
@@ -1348,9 +1348,8 @@ typedef struct SpecNextResult {
} SpecNextResult;
int calendar_spec_next_usec(const CalendarSpec *spec, usec_t usec, usec_t *next) {
SpecNextResult *shared, tmp;
pid_t pid;
SpecNextResult *shared;
SpecNextResult tmp;
int r;
if (isempty(spec->timezone))
@@ -1360,15 +1359,12 @@ int calendar_spec_next_usec(const CalendarSpec *spec, usec_t usec, usec_t *next)
if (shared == MAP_FAILED)
return negative_errno();
pid = fork();
if (pid == -1) {
int fork_errno = errno;
r = safe_fork("(sd-calendar)", 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) {
if (setenv("TZ", spec->timezone, 1) != 0) {
shared->return_value = negative_errno();
_exit(EXIT_FAILURE);
+9 -16
View File
@@ -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;
@@ -222,11 +216,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);
}
+137
View File
@@ -56,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"
@@ -1134,6 +1135,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;
}
static const char *const ioprio_class_table[] = {
[IOPRIO_CLASS_NONE] = "none",
[IOPRIO_CLASS_RT] = "realtime",
+14
View File
@@ -142,3 +142,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);
}
+8 -10
View File
@@ -1106,11 +1106,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]);
@@ -1157,11 +1156,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]);
+5 -8
View File
@@ -886,8 +886,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))
@@ -900,15 +900,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) {
+7 -39
View File
@@ -186,11 +186,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);
@@ -198,45 +198,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);