chiark / gitweb /
fd-util: add new call rearrange_stdio()
authorLennart Poettering <lennart@poettering.net>
Wed, 28 Feb 2018 09:00:26 +0000 (10:00 +0100)
committerSven Eden <yamakuzure@gmx.net>
Wed, 30 May 2018 05:59:13 +0000 (07:59 +0200)
Quite often we need to set up a number of fds as stdin/stdout/stderr of
a process we are about to start. Add a generic implementation for a
routine doing that that takes care to do so properly:

1. Can handle the case where stdin/stdout/stderr where previously
   closed, and the fds to set as stdin/stdout/stderr hence likely in the
   0..2 range.  handling this properly is nasty, since we need to first
   move the fds out of this range in order to later move them back in, to
   make things fully robust.

2. Can optionally open /dev/null in case for one or more of the fds, in
   a smart way, sharing the open file if possible between multiple of
   the fds.

3. Guarantees that O_CLOEXEC is not set on the three fds, even if the fds
   already were in the 0..2 range and hence possibly weren't moved.

src/basic/fd-util.c
src/basic/fd-util.h
src/test/test-fd-util.c

index 8eb65312a906e749f26a81a11477d8224e004bf6..87a35d35c72d0cd08dbca75ab3b7b2067e3b4baf 100644 (file)
@@ -619,3 +619,118 @@ int fd_move_above_stdio(int fd) {
         (void) close(fd);
         return copy;
 }
+
+int rearrange_stdio(int original_input_fd, int original_output_fd, int original_error_fd) {
+
+        int fd[3] = { /* Put together an array of fds we work on */
+                original_input_fd,
+                original_output_fd,
+                original_error_fd
+        };
+
+        int r, i,
+                null_fd = -1,                /* if we open /dev/null, we store the fd to it here */
+                copy_fd[3] = { -1, -1, -1 }; /* This contains all fds we duplicate here temporarily, and hence need to close at the end */
+        bool null_readable, null_writable;
+
+        /* Sets up stdin, stdout, stderr with the three file descriptors passed in. If any of the descriptors is
+         * specified as -1 it will be connected with /dev/null instead. If any of the file descriptors is passed as
+         * itself (e.g. stdin as STDIN_FILENO) it is left unmodified, but the O_CLOEXEC bit is turned off should it be
+         * on.
+         *
+         * Note that if any of the passed file descriptors are > 2 they will be closed — both on success and on
+         * failure! Thus, callers should assume that when this function returns the input fds are invalidated.
+         *
+         * Note that when this function fails stdin/stdout/stderr might remain half set up!
+         *
+         * O_CLOEXEC is turned off for all three file descriptors (which is how it should be for
+         * stdin/stdout/stderr). */
+
+        null_readable = original_input_fd < 0;
+        null_writable = original_output_fd < 0 || original_error_fd < 0;
+
+        /* First step, open /dev/null once, if we need it */
+        if (null_readable || null_writable) {
+
+                /* Let's open this with O_CLOEXEC first, and convert it to non-O_CLOEXEC when we move the fd to the final position. */
+                null_fd = open("/dev/null", (null_readable && null_writable ? O_RDWR :
+                                             null_readable ? O_RDONLY : O_WRONLY) | O_CLOEXEC);
+                if (null_fd < 0) {
+                        r = -errno;
+                        goto finish;
+                }
+
+                /* If this fd is in the 0…2 range, let's move it out of it */
+                if (null_fd < 3) {
+                        int copy;
+
+                        copy = fcntl(null_fd, F_DUPFD_CLOEXEC, 3); /* Duplicate this with O_CLOEXEC set */
+                        if (copy < 0) {
+                                r = -errno;
+                                goto finish;
+                        }
+
+                        safe_close(null_fd);
+                        null_fd = copy;
+                }
+        }
+
+        /* Let's assemble fd[] with the fds to install in place of stdin/stdout/stderr */
+        for (i = 0; i < 3; i++) {
+
+                if (fd[i] < 0)
+                        fd[i] = null_fd;        /* A negative parameter means: connect this one to /dev/null */
+                else if (fd[i] != i && fd[i] < 3) {
+                        /* This fd is in the 0…2 territory, but not at its intended place, move it out of there, so that we can work there. */
+                        copy_fd[i] = fcntl(fd[i], F_DUPFD_CLOEXEC, 3); /* Duplicate this with O_CLOEXEC set */
+                        if (copy_fd[i] < 0) {
+                                r = -errno;
+                                goto finish;
+                        }
+
+                        fd[i] = copy_fd[i];
+                }
+        }
+
+        /* At this point we now have the fds to use in fd[], and they are all above the stdio range, so that we
+         * have freedom to move them around. If the fds already were at the right places then the specific fds are
+         * -1. Let's now move them to the right places. This is the point of no return. */
+        for (i = 0; i < 3; i++) {
+
+                if (fd[i] == i) {
+
+                        /* fd is already in place, but let's make sure O_CLOEXEC is off */
+                        r = fd_cloexec(i, false);
+                        if (r < 0)
+                                goto finish;
+
+                } else {
+                        assert(fd[i] > 2);
+
+                        if (dup2(fd[i], i) < 0) { /* Turns off O_CLOEXEC on the new fd. */
+                                r = -errno;
+                                goto finish;
+                        }
+                }
+        }
+
+        r = 0;
+
+finish:
+        /* Close the original fds, but only if they were outside of the stdio range. Also, properly check for the same
+         * fd passed in multiple times. */
+        safe_close_above_stdio(original_input_fd);
+        if (original_output_fd != original_input_fd)
+                safe_close_above_stdio(original_output_fd);
+        if (original_error_fd != original_input_fd && original_error_fd != original_output_fd)
+                safe_close_above_stdio(original_error_fd);
+
+        /* Close the copies we moved > 2 */
+        for (i = 0; i < 3; i++)
+                safe_close(copy_fd[i]);
+
+        /* Close our null fd, if it's > 2 */
+        safe_close_above_stdio(null_fd);
+
+        return r;
+}
index 7c10097736ed28bb815955978538267bbe0567c7..f53839d4560198fc304cb5c2129725cb46e51069 100644 (file)
@@ -104,3 +104,5 @@ int acquire_data_fd(const void *data, size_t size, unsigned flags);
         IN_SET(r, ENOTCONN, ECONNRESET, ECONNREFUSED, ECONNABORTED, EPIPE, ENETUNREACH)
 
 int fd_move_above_stdio(int fd);
+
+int rearrange_stdio(int original_input_fd, int original_output_fd, int original_error_fd);
index 76b36053b935b74ccfbe7f7374cd7b63ebda9663..186fca733f1e50cb0f31a717451ae196cd3e7f64 100644 (file)
@@ -25,6 +25,8 @@
 #include "fd-util.h"
 #include "fileio.h"
 #include "macro.h"
+//#include "path-util.h"
+//#include "process-util.h"
 #include "random-util.h"
 #include "string-util.h"
 #include "util.h"
@@ -175,6 +177,72 @@ static void test_fd_move_above_stdio(void) {
         assert_se(close_nointr(new_fd) != EBADF);
 }
 
+static void test_rearrange_stdio(void) {
+        pid_t pid;
+        int r;
+
+        r = safe_fork("rearrange", FORK_WAIT|FORK_LOG, &pid);
+        assert_se(r >= 0);
+
+        if (r == 0) {
+                _cleanup_free_ char *path = NULL;
+                char buffer[10];
+
+                /* Child */
+
+                safe_close(STDERR_FILENO); /* Let's close an fd < 2, to make it more interesting */
+
+                assert_se(rearrange_stdio(-1, -1, -1) >= 0);
+
+                assert_se(fd_get_path(STDIN_FILENO, &path) >= 0);
+                assert_se(path_equal(path, "/dev/null"));
+                path = mfree(path);
+
+                assert_se(fd_get_path(STDOUT_FILENO, &path) >= 0);
+                assert_se(path_equal(path, "/dev/null"));
+                path = mfree(path);
+
+                assert_se(fd_get_path(STDOUT_FILENO, &path) >= 0);
+                assert_se(path_equal(path, "/dev/null"));
+                path = mfree(path);
+
+                safe_close(STDIN_FILENO);
+                safe_close(STDOUT_FILENO);
+                safe_close(STDERR_FILENO);
+
+                {
+                        int pair[2];
+                        assert_se(pipe(pair) >= 0);
+                        assert_se(pair[0] == 0);
+                        assert_se(pair[1] == 1);
+                        assert_se(fd_move_above_stdio(0) == 3);
+                }
+                assert_se(open("/dev/full", O_WRONLY|O_CLOEXEC) == 0);
+                assert_se(acquire_data_fd("foobar", 6, 0) == 2);
+
+                assert_se(rearrange_stdio(2, 0, 1) >= 0);
+
+                assert_se(write(1, "x", 1) < 0 && errno == ENOSPC);
+                assert_se(write(2, "z", 1) == 1);
+                assert_se(read(3, buffer, sizeof(buffer)) == 1);
+                assert_se(buffer[0] == 'z');
+                assert_se(read(0, buffer, sizeof(buffer)) == 6);
+                assert_se(memcmp(buffer, "foobar", 6) == 0);
+
+                assert_se(rearrange_stdio(-1, 1, 2) >= 0);
+                assert_se(write(1, "a", 1) < 0 && errno == ENOSPC);
+                assert_se(write(2, "y", 1) == 1);
+                assert_se(read(3, buffer, sizeof(buffer)) == 1);
+                assert_se(buffer[0] == 'y');
+
+                assert_se(fd_get_path(0, &path) >= 0);
+                assert_se(path_equal(path, "/dev/null"));
+                path = mfree(path);
+
+                _exit(EXIT_SUCCESS);
+        }
+}
+
 int main(int argc, char *argv[]) {
         test_close_many();
         test_close_nointr();
@@ -184,6 +252,7 @@ int main(int argc, char *argv[]) {
         test_open_serialization_fd();
         test_acquire_data_fd();
         test_fd_move_above_stdio();
+        test_rearrange_stdio();
 
         return 0;
 }