chiark / gitweb /
tree-wide: introduce new safe_fork() helper and port everything over
authorLennart Poettering <lennart@poettering.net>
Fri, 22 Dec 2017 12:08:14 +0000 (13:08 +0100)
committerSven Eden <yamakuzure@gmx.net>
Wed, 30 May 2018 05:49:44 +0000 (07:49 +0200)
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.

src/basic/exec-util.c
src/basic/process-util.c
src/basic/process-util.h
src/basic/terminal-util.c
src/basic/time-util.c
src/basic/util.c
src/libelogind/sd-bus/bus-container.c
src/libelogind/sd-bus/bus-socket.c
src/login/inhibit.c
src/shared/pager.c
src/test/test-process-util.c

index 718fbf7eec94cfbdfc36d500a4b0b87bee41ecb6..19ac3686cdcc595e04b4c19044087ea2a99218f9 100644 (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;
@@ -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);
         }
index 2494ba6afca1f25e186c7e86a68635ef6bf5cbe3..900e42ee8655a0841ef2f497b42d425c1e2c2a13 100644 (file)
@@ -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",
index a84a96862dc6ef7e5a6fa38fc1d8973ac4c67ce0..ffd4096e506146dbe4caf3fd698df107653ed4c8 100644 (file)
@@ -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);
+}
index c67b34073e381e50dbc1fb82417ed9cfdadc0110..9a7b18ceba5ddcef40789dda30a2baae6b57045f 100644 (file)
@@ -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]);
index c72094188cdbcd9c6c27c6d949e319a2c752cbce..a6bff575cfa92887a8b8bd3dec8684be62563b47 100644 (file)
@@ -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) {
index c58bf5601bf62a23b98309d5d916dd061ea6102e..51b2a9d146c5e3e6e0f87439bf770aedd991d9e7 100644 (file)
@@ -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);
index 8f6d34838e2aaca3ebe64e73819dc1ed22fc3ea2..a1242c5678b0e78e7d66873a8bcc8aa4b93f5780 100644 (file)
@@ -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) {
index d72cb616e000846b1de253c0d96b3a21b3cc97ad..013bdb05a002adc983eda5fdc8fe3cd8d891a652 100644 (file)
@@ -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);
 
index 7c48e7cee148b351699181b11470c0bf9edd8286..3ea5124b3a702d3e7a35b34fe699695083c34d54 100644 (file)
@@ -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);
index cdf6609a8f314cc751b12300da653ebca5e5a8e8..1f0d0b8872c0cd437765aabeb9503a2f77d52c40 100644 (file)
@@ -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);
index 0cb9a82d34e27c2f663691299ea9303ada2a9664..375001dbbfb95f19a27d8f0fe43858d416c30669 100644 (file)
@@ -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;
 }