lib/util: add tfork()
authorStefan Metzmacher <metze@samba.org>
Thu, 23 Sep 2010 16:10:02 +0000 (18:10 +0200)
committerRalph Boehme <slow@samba.org>
Thu, 20 Apr 2017 14:53:16 +0000 (16:53 +0200)
triple-fork to avoid handling SIGCHLD in the parent.

This function is a workaround for the problem of using fork() in
library code. In that case the library should avoid setting a global
signal handler for SIGCHLD, because the application may wants to use its
own handler.

status_fd can be used to wait for the child to exit and get its exit
status.

Signed-off-by: Ralph Boehme <slow@samba.org>
Reviewed-by: Jeremy Allison <jra@samba.org>
lib/util/tfork.c [new file with mode: 0644]
lib/util/tfork.h [new file with mode: 0644]
lib/util/wscript_build

diff --git a/lib/util/tfork.c b/lib/util/tfork.c
new file mode 100644 (file)
index 0000000..27b6cc0
--- /dev/null
@@ -0,0 +1,326 @@
+/*
+   fork on steroids to avoid SIGCHLD and waitpid
+
+   Copyright (C) Stefan Metzmacher 2010
+   Copyright (C) Ralph Boehme 2017
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "replace.h"
+#include "system/wait.h"
+#include "system/filesys.h"
+#include "lib/util/samba_util.h"
+#include "lib/util/sys_rw.h"
+#include "lib/util/tfork.h"
+#include "lib/util/debug.h"
+
+struct tfork_state {
+       void (*old_sig_chld)(int);
+       int status_pipe[2];
+       pid_t *parent;
+
+       pid_t level0_pid;
+       int level0_status;
+
+       pid_t level1_pid;
+       int level1_errno;
+
+       pid_t level2_pid;
+       int level2_errno;
+
+       pid_t level3_pid;
+};
+
+/*
+ * TODO: We should make this global thread local
+ */
+static struct tfork_state *tfork_global;
+
+static void tfork_sig_chld(int signum)
+{
+       if (tfork_global->level1_pid > 0) {
+               int ret = waitpid(tfork_global->level1_pid,
+                             &tfork_global->level0_status,
+                             WNOHANG);
+               if (ret == tfork_global->level1_pid) {
+                       tfork_global->level1_pid = -1;
+                       return;
+               }
+       }
+
+       /*
+        * Not our child, forward to old handler
+        */
+
+       if (tfork_global->old_sig_chld == SIG_IGN) {
+               return;
+       }
+
+       if (tfork_global->old_sig_chld == SIG_DFL) {
+               return;
+       }
+
+       tfork_global->old_sig_chld(signum);
+}
+
+static pid_t level2_fork_and_wait(int child_ready_fd)
+{
+       int status;
+       ssize_t written;
+       pid_t pid;
+       int fd;
+       bool wait;
+
+       /*
+        * Child level 2.
+        *
+        * Do a final fork and if the tfork() caller passed a status_fd, wait
+        * for child3 and return its exit status via status_fd.
+        */
+
+       pid = fork();
+       if (pid == 0) {
+               /*
+                * Child level 3, this one finally returns from tfork() as child
+                * with pid 0.
+                *
+                * Cleanup all ressources we allocated before returning.
+                */
+               close(child_ready_fd);
+               close(tfork_global->status_pipe[1]);
+
+               if (tfork_global->parent != NULL) {
+                       /*
+                        * we're in the child and return the level0 parent pid
+                        */
+                       *tfork_global->parent = tfork_global->level0_pid;
+               }
+
+               anonymous_shared_free(tfork_global);
+               tfork_global = NULL;
+
+               return 0;
+       }
+
+       tfork_global->level3_pid = pid;
+       if (tfork_global->level3_pid == -1) {
+               tfork_global->level2_errno = errno;
+               _exit(0);
+       }
+
+       sys_write(child_ready_fd, &(char){0}, 1);
+
+       if (tfork_global->status_pipe[1] == -1) {
+               _exit(0);
+       }
+       wait = true;
+
+       /*
+        * We're going to stay around until child3 exits, so lets close all fds
+        * other then the pipe fd we may have inherited from the caller.
+        */
+       fd = dup2(tfork_global->status_pipe[1], 0);
+       if (fd == -1) {
+               status = errno;
+               kill(tfork_global->level3_pid, SIGKILL);
+               wait = false;
+       }
+       closefrom(1);
+
+       while (wait) {
+               int ret = waitpid(tfork_global->level3_pid, &status, 0);
+               if (ret == -1) {
+                       if (errno == EINTR) {
+                               continue;
+                       }
+                       status = errno;
+               }
+               break;
+       }
+
+       written = sys_write(fd, &status, sizeof(status));
+       if (written != sizeof(status)) {
+               abort();
+       }
+
+       _exit(0);
+}
+
+pid_t tfork(int *status_fd, pid_t *parent)
+{
+       int ret;
+       pid_t pid;
+       pid_t child;
+
+       tfork_global = (struct tfork_state *)
+               anonymous_shared_allocate(sizeof(struct tfork_state));
+       if (tfork_global == NULL) {
+               return -1;
+       }
+
+       tfork_global->parent = parent;
+       tfork_global->status_pipe[0] = -1;
+       tfork_global->status_pipe[1] = -1;
+
+       tfork_global->level0_pid = getpid();
+       tfork_global->level0_status = -1;
+       tfork_global->level1_pid = -1;
+       tfork_global->level1_errno = ECANCELED;
+       tfork_global->level2_pid = -1;
+       tfork_global->level2_errno = ECANCELED;
+       tfork_global->level3_pid = -1;
+
+       if (status_fd != NULL) {
+               ret = pipe(&tfork_global->status_pipe[0]);
+               if (ret != 0) {
+                       int saved_errno = errno;
+
+                       anonymous_shared_free(tfork_global);
+                       tfork_global = NULL;
+                       errno = saved_errno;
+                       return -1;
+               }
+
+               *status_fd = tfork_global->status_pipe[0];
+       }
+
+       /*
+        * We need to set our own signal handler to prevent any existing signal
+        * handler from reaping our child.
+        */
+       tfork_global->old_sig_chld = CatchSignal(SIGCHLD, tfork_sig_chld);
+
+       pid = fork();
+       if (pid == 0) {
+               int level2_pipe[2];
+               char c;
+               ssize_t nread;
+
+               /*
+                * Child level 1.
+                *
+                * Restore SIGCHLD handler
+                */
+               CatchSignal(SIGCHLD, SIG_DFL);
+
+               /*
+                * Close read end of the signal pipe, we don't need it anymore
+                * and don't want to leak it into childs.
+                */
+               if (tfork_global->status_pipe[0] != -1) {
+                       close(tfork_global->status_pipe[0]);
+                       tfork_global->status_pipe[0] = -1;
+               }
+
+               /*
+                * Create a pipe for waiting for the child level 2 to finish
+                * forking.
+                */
+               ret = pipe(&level2_pipe[0]);
+               if (ret != 0) {
+                       tfork_global->level1_errno = errno;
+                       _exit(0);
+               }
+
+               pid = fork();
+               if (pid == 0) {
+
+                       /*
+                        * Child level 2.
+                        */
+
+                       close(level2_pipe[0]);
+                       return level2_fork_and_wait(level2_pipe[1]);
+               }
+
+               tfork_global->level2_pid = pid;
+               if (tfork_global->level2_pid == -1) {
+                       tfork_global->level1_errno = errno;
+                       _exit(0);
+               }
+
+               close(level2_pipe[1]);
+               level2_pipe[1] = -1;
+
+               nread = sys_read(level2_pipe[0], &c, 1);
+               if (nread != 1) {
+                       abort();
+               }
+               _exit(0);
+       }
+
+       tfork_global->level1_pid = pid;
+       if (tfork_global->level1_pid == -1) {
+               int saved_errno = errno;
+
+               anonymous_shared_free(tfork_global);
+               tfork_global = NULL;
+               errno = saved_errno;
+               return -1;
+       }
+
+       /*
+        * By using the helper variable pid we avoid a TOCTOU with the signal
+        * handler that will set tfork_global->level1_pid to -1 (which would
+        * cause waitpid() to block waiting for another exitted child).
+        *
+        * We can't avoid the race waiting for pid twice (in the signal handler
+        * and then again here in the while loop), but we must avoid waiting for
+        * -1 and this does the trick.
+        */
+       pid = tfork_global->level1_pid;
+
+       while (tfork_global->level1_pid != -1) {
+               ret = waitpid(pid, &tfork_global->level0_status, 0);
+               if (ret == -1 && errno == EINTR) {
+                       continue;
+               }
+
+               break;
+       }
+
+       CatchSignal(SIGCHLD, tfork_global->old_sig_chld);
+
+       if (tfork_global->level0_status != 0) {
+               anonymous_shared_free(tfork_global);
+               tfork_global = NULL;
+               errno = ECHILD;
+               return -1;
+       }
+
+       if (tfork_global->level2_pid == -1) {
+               int saved_errno = tfork_global->level1_errno;
+
+               anonymous_shared_free(tfork_global);
+               tfork_global = NULL;
+               errno = saved_errno;
+               return -1;
+       }
+
+       if (tfork_global->level3_pid == -1) {
+               int saved_errno = tfork_global->level2_errno;
+
+               anonymous_shared_free(tfork_global);
+               tfork_global = NULL;
+               errno = saved_errno;
+               return -1;
+       }
+
+       child = tfork_global->level3_pid;
+       anonymous_shared_free(tfork_global);
+       tfork_global = NULL;
+
+       return child;
+}
diff --git a/lib/util/tfork.h b/lib/util/tfork.h
new file mode 100644 (file)
index 0000000..0c62fc3
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+   fork on steroids to avoid SIGCHLD and waitpid
+
+   Copyright (C) Stefan Metzmacher 2010
+   Copyright (C) Ralph Boehme 2017
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef LIB_UTIL_TFORK_H
+#define LIB_UTIL_TFORK_H
+
+/**
+ * @brief a fork() that avoids SIGCHLD and waitpid
+ *
+ * This function is a workaround for the problem of using fork() in
+ * library code. In that case the library should avoid to set a global
+ * signal handler for SIGCHLD, because the application may wants to use its
+ * own handler.
+ *
+ * The child process will start with SIGCHLD handler set to SIG_DFL, so the
+ * child might need to setup its own handler.
+ *
+ * @param[out] status_fd  If this is not NULL, tfork creates a pipe and returns
+ *                        the readable end via this pointer. The caller can
+ *                        wait for the process to finish by polling the
+ *                        status_fd for readability and can then read the exit
+ *                        status (an int).
+ *
+ * @param[out] parent     The PID of the parent process, if 0 is returned
+ *                        otherwise the variable will not be touched at all.
+ *                        It is possible to pass NULL.
+ *
+ * @return                On success, the PID of the child process is returned
+ *                        in the parent, and 0 is returned in the child. On
+ *                        failure, -1 is returned in the parent, no child
+ *                        process is created, and errno is set appropriately.
+ */
+int tfork(int *status_fd, int *parent);
+
+#endif /* LIB_UTIL_TFORK_H */
index d093947..1d2f2ba 100644 (file)
@@ -121,10 +121,10 @@ else:
                     idtree_random.c base64.c
                     util_str.c util_str_common.c ms_fnmatch.c
                     server_id.c dprintf.c pidfile.c
-                    tevent_debug.c memcache.c unix_match.c''',
+                    tevent_debug.c memcache.c unix_match.c tfork.c''',
                   deps='samba-util-core DYNCONFIG close-low-fd tini tiniparser genrand',
                   public_deps='talloc tevent execinfo pthread LIBCRYPTO charset util_setid systemd systemd-daemon',
-                  public_headers='debug.h attr.h byteorder.h data_blob.h memory.h safe_string.h time.h talloc_stack.h string_wrappers.h idtree.h idtree_random.h blocking.h signal.h substitute.h fault.h genrand.h',
+                  public_headers='debug.h attr.h byteorder.h data_blob.h memory.h safe_string.h time.h talloc_stack.h string_wrappers.h idtree.h idtree_random.h blocking.h signal.h substitute.h fault.h genrand.h tfork.h',
                   header_path= [ ('dlinklist.h samba_util.h', '.'), ('*', 'util') ],
                   local_include=False,
                   vnum='0.0.1',