]> git.ozlabs.org Git - ccan/commitdiff
closefrom: Close all file descriptors above a certain value.
authorZmnSCPxj jxPCSnmZ <ZmnSCPxj@protonmail.com>
Tue, 19 Oct 2021 01:51:43 +0000 (09:51 +0800)
committerRusty Russell <rusty@rustcorp.com.au>
Thu, 21 Oct 2021 04:28:22 +0000 (14:58 +1030)
For more information: https://github.com/ElementsProject/lightning/issues/4868

Signed-off-by: ZmnSCPxj jxPCSnmZ <ZmnSCPxj@protonmail.com>
ccan/closefrom/LICENSE [new symlink]
ccan/closefrom/_info [new file with mode: 0644]
ccan/closefrom/closefrom.c [new file with mode: 0644]
ccan/closefrom/closefrom.h [new file with mode: 0644]
ccan/closefrom/test/run.c [new file with mode: 0644]
tools/configurator/configurator.c

diff --git a/ccan/closefrom/LICENSE b/ccan/closefrom/LICENSE
new file mode 120000 (symlink)
index 0000000..b7951da
--- /dev/null
@@ -0,0 +1 @@
+../../licenses/CC0
\ No newline at end of file
diff --git a/ccan/closefrom/_info b/ccan/closefrom/_info
new file mode 100644 (file)
index 0000000..28f903a
--- /dev/null
@@ -0,0 +1,69 @@
+#include "config.h"
+#include <stdio.h>
+#include <string.h>
+
+/**
+ * closefrom - close all fds starting from specified fd.
+ *
+ * This code is an example of what to do in a child process to
+ * ensure that none of the (possibly sensitive) file descriptors
+ * in the parent remain in the child process.
+ *
+ * License: CC0 (Public domain)
+ * Author: ZmnSCPxj jxPCSnmZ <ZmnSCPxj@protonmail.com>
+ *
+ * Example:
+ * #include <ccan/closefrom/closefrom.h>
+ * #include <ccan/err/err.h>
+ * #include <stdio.h>
+ * #include <sys/resource.h>
+ * #include <sys/time.h>
+ * #include <sys/types.h>
+ * #include <sys/wait.h>
+ * #include <unistd.h>
+ *
+ * int main(int argc, char **argv)
+ * {
+ *     pid_t child;
+ *
+ *     // If being emulated, then we might end up
+ *     // looping over a large _SC_OPEN_MAX
+ *     // (Some systems have it as INT_MAX!)
+ *     // If so, closefrom_limit will lower this limit
+ *     // to a value you specify, or if given 0 will
+ *     // limit to 4096.
+ *     // Call this as early as possible.
+ *     closefrom_limit(0);
+ *
+ *     // If we limited, we can query this so we can
+ *     // print it in debug logs or something.
+ *     if (close_from_may_be_slow())
+ *             printf("we limited ourselves to 4096 fds.\n");
+ *
+ *     child = fork();
+ *     if (child < 0)
+ *             err(1, "Forking");
+ *     if (child == 0) {
+ *             closefrom(STDERR_FILENO + 1);
+ *             // Insert your *whatever* code here.
+ *             _exit(0);
+ *     }
+ *
+ *     waitpid(child, NULL, 0);
+ *
+ *     return 0;
+ * }
+ *
+ */
+int main(int argc, char *argv[])
+{
+       /* Expect exactly one argument */
+       if (argc != 2)
+               return 1;
+
+       if (strcmp(argv[1], "depends") == 0) {
+               return 0;
+       }
+
+       return 1;
+}
diff --git a/ccan/closefrom/closefrom.c b/ccan/closefrom/closefrom.c
new file mode 100644 (file)
index 0000000..177b05e
--- /dev/null
@@ -0,0 +1,225 @@
+/* CC0 license (public domain) - see LICENSE file for details */
+#include <ccan/closefrom/closefrom.h>
+#include <dirent.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+/* See also:
+ * https://stackoverflow.com/a/918469
+ *
+ * The implementation below is not exhaustive of all the suggested above.
+ */
+
+#if !HAVE_CLOSEFROM
+
+/* IBM AIX.
+ * https://www.ibm.com/docs/en/aix/7.2?topic=f-fcntl-dup-dup2-subroutine
+ */
+#if HAVE_F_CLOSEM
+
+#include <fcntl.h>
+
+void closefrom(int fromfd)
+{
+       (void) fcntl(fromfd, F_CLOSEM, 0);
+}
+
+bool closefrom_may_be_slow(void)
+{
+       return false;
+}
+
+#else /* !HAVE_F_CLOSEM */
+
+#if HAVE_NR_CLOSE_RANGE
+#include <sys/syscall.h>
+#endif
+
+#define PROC_PID_FD_LEN \
+       ( 6  /* /proc/ */ \
+       + 20 /* 64-bit $PID */ \
+       + 3  /* /fd */ \
+       + 1  /* NUL */ \
+       )
+
+static bool can_get_maxfd(void)
+{
+#if HAVE_F_MAXFD
+       int res = fcntl(0, F_MAXFD);
+       if (res < 0)
+               return false;
+       else
+               return true;
+#else
+       return false;
+#endif
+}
+
+/* Linux >= 5.9 */
+static bool can_close_range(void)
+{
+#if HAVE_NR_CLOSE_RANGE
+       int res = syscall(__NR_close_range, INT_MAX, INT_MAX, 0);
+       if (res < 0)
+               return false;
+       return true;
+#else
+       return false;
+#endif
+}
+
+/* On Linux, Solaris, AIX, Cygwin, and NetBSD.  */
+static bool can_open_proc_pid_fd(void)
+{
+       char dnam[PROC_PID_FD_LEN];
+       DIR *dir;
+
+       sprintf(dnam, "/proc/%ld/fd", (long) getpid());
+       dir = opendir(dnam);
+       if (!dir)
+               return false;
+       closedir(dir);
+       return true;
+}
+
+/* On FreeBSD and MacOS.  */
+static bool can_open_dev_fd(void)
+{
+       DIR *dir;
+       dir = opendir("/dev/fd");
+       if (!dir)
+               return false;
+       closedir(dir);
+       return true;
+}
+
+bool closefrom_may_be_slow(void)
+{
+       if (can_get_maxfd())
+               return false;
+       else if (can_close_range())
+               return false;
+       else if (can_open_proc_pid_fd())
+               return false;
+       else if (can_open_dev_fd())
+               return false;
+       else
+               return true;
+}
+
+/* It is possible that we run out of available file descriptors.
+ * However, if we are going to close anyway, we could just try
+ * closing file descriptors until we reach maxfd.
+ */
+static
+DIR *try_opendir(const char *dnam, int *fromfd, int maxfd)
+{
+       DIR *dir;
+
+       do {
+               dir = opendir(dnam);
+               if (!dir && (errno == ENFILE || errno == EMFILE)) {
+                       if (*fromfd < maxfd)
+                               close((*fromfd)++);
+                       else
+                               break;
+               }
+       } while (!dir && (errno == ENFILE || errno == EMFILE));
+
+       return dir;
+}
+
+void closefrom(int fromfd)
+{
+       int saved_errno = errno;
+
+       int res;
+       int maxfd;
+
+       char dnam[PROC_PID_FD_LEN];
+       DIR *dir;
+       struct dirent *entry;
+
+       (void) res;
+
+       if (fromfd < 0)
+               goto quit;
+
+#if HAVE_NR_CLOSE_RANGE
+       res = syscall(__NR_close_range, fromfd, INT_MAX, 0);
+       if (res == 0)
+               goto quit;
+#endif
+
+       maxfd = sysconf(_SC_OPEN_MAX);
+
+       sprintf(dnam, "/proc/%ld/fd", (long) getpid());
+       dir = try_opendir(dnam, &fromfd, maxfd);
+       if (!dir)
+               dir = try_opendir("/dev/fd", &fromfd, maxfd);
+
+       if (dir) {
+               while ((entry = readdir(dir))) {
+                       long fd;
+                       char *endp;
+
+                       fd = strtol(entry->d_name, &endp, 10);
+                       if (entry->d_name != endp && *endp == '\0' &&
+                           fd >= 0 && fd < INT_MAX && fd >= fromfd &&
+                           fd != dirfd(dir) )
+                               close(fd);
+               }
+               closedir(dir);
+               goto quit;
+       }
+
+#if HAVE_F_MAXFD
+       res = fcntl(0, F_MAXFD);
+       if (res >= 0)
+               maxfd = res + 1;
+#endif
+
+       /* Fallback.  */
+       for (; fromfd < maxfd; ++fromfd)
+               close(fromfd);
+
+quit:
+       errno = saved_errno;
+}
+
+#endif /* !HAVE_F_CLOSEM */
+
+void closefrom_limit(unsigned int arg_limit)
+{
+       rlim_t limit = (rlim_t) arg_limit;
+
+       struct rlimit nofile;
+
+       if (!closefrom_may_be_slow())
+               return;
+
+       if (limit == 0)
+               limit = 4096;
+
+       getrlimit(RLIMIT_NOFILE, &nofile);
+
+       /* Respect the max limit.
+        * If we are not running as root then we cannot raise
+        * it, but we *can* lower the max limit.
+        */
+       if (nofile.rlim_max != RLIM_INFINITY && limit > nofile.rlim_max)
+               limit = nofile.rlim_max;
+
+       nofile.rlim_cur = limit;
+       nofile.rlim_max = limit;
+
+       setrlimit(RLIMIT_NOFILE, &nofile);
+}
+
+#endif /* !HAVE_CLOSEFROM */
diff --git a/ccan/closefrom/closefrom.h b/ccan/closefrom/closefrom.h
new file mode 100644 (file)
index 0000000..790a5ba
--- /dev/null
@@ -0,0 +1,81 @@
+/* CC0 license (public domain) - see LICENSE file for details */
+#ifndef CCAN_CLOSEFROM_H
+#define CCAN_CLOSEFROM_H
+#include "config.h"
+#include <stdbool.h>
+
+#if HAVE_CLOSEFROM
+/* BSD.  */
+#include <unistd.h>
+/* Solaris.  */
+#include <stdlib.h>
+
+static inline
+bool closefrom_may_be_slow(void)
+{
+       return 0;
+}
+
+static inline
+void closefrom_limit(unsigned int limit)
+{
+}
+
+#else /* !HAVE_CLOSEFROM */
+
+/**
+ * closefrom - Close all open file descriptors, starting
+ * at fromfd onwards.
+ * @fromfd: the first fd to close; it and all higher file descriptors
+ * will be closed.
+ *
+ * This is not multithread-safe: other threads in the same process
+ * may or may not open new file descriptors in parallel to this call.
+ * However, the expected use-case is that this will be called in a
+ * child process just after fork(), meaning the child process is still
+ * single-threaded.
+ */
+void closefrom_(int fromfd);
+/* In case the standard library has it, but declared in some
+ * *other* header we do not know of yet, we use closefrom_ in
+ * the actual name the linker sees.
+ */
+#define closefrom closefrom_
+
+/**
+ * closefrom_may_be_slow - check if the closefrom() function could
+ * potentially take a long time.
+ *
+ * The return value is true if closefrom() is emulated by
+ * looping from fromfd to sysconf(_SC_OPEN_MAX), which can be
+ * very large (possibly even INT_MAX on some systems).
+ * If so, you might want to use setrlimit to limit _SC_OPEN_MAX.
+ * If this returns false, then closefrom is efficient and you do not
+ * need to limit the number of file descriptors.
+ *
+ * You can use closefrom_limit to perform the limiting based on
+ * closefrom_may_be_slow.
+ * This API is exposed in case you want to output to debug logs or
+ * something similar.
+ */
+bool closefrom_may_be_slow(void);
+
+/**
+ * closefrom_limit - If closefrom_may_be_slow(), lower the limit on
+ * the number of file descriptors we keep open, to prevent closefrom
+ * from being *too* slow.
+ * @limit: 0 to use a reasonable default of 4096, or non-zero for the
+ * limit you prefer.
+ *
+ * This function does nothing if closefrom_may_be_slow() return false.
+ *
+ * This function only *lowers* the limit from the hard limit set by
+ * root before running this program.
+ * If the limit is higher than the hard limit, then the hard limit is
+ * respected.
+ */
+void closefrom_limit(unsigned int limit);
+
+#endif /* !HAVE_CLOSEFROM */
+
+#endif /* CCAN_CLOSEFROM_H */
diff --git a/ccan/closefrom/test/run.c b/ccan/closefrom/test/run.c
new file mode 100644 (file)
index 0000000..aaae410
--- /dev/null
@@ -0,0 +1,192 @@
+#include <ccan/closefrom/closefrom.h>
+/* Include the C files directly. */
+#include <ccan/closefrom/closefrom.c>
+#include <ccan/tap/tap.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+/* Open a pipe, do closefrom, check pipe no longer works.   */
+static
+int pipe_close(void)
+{
+       int fds[2];
+       ssize_t wres;
+
+       char buf = '\0';
+
+       if (pipe(fds) < 0)
+               return 0;
+
+       /* Writing to the write end should succeed, the
+        * pipe is working.  */
+       do {
+               wres = write(fds[1], &buf, 1);
+       } while ((wres < 0) && (errno == EINTR));
+       if (wres < 0)
+               return 0;
+
+       closefrom(STDERR_FILENO + 1);
+
+       /* Writing to the write end should fail because
+        * everything should be closed.  */
+       do {
+               wres = write(fds[1], &buf, 1);
+       } while ((wres < 0) && (errno == EINTR));
+
+       return (wres < 0) && (errno == EBADF);
+}
+
+/* Open a pipe, fork, do closefrom in child, read pipe from parent,
+ * parent should see EOF.
+ */
+static
+int fork_close(void)
+{
+       int fds[2];
+       pid_t child;
+
+       char buf;
+       ssize_t rres;
+
+       if (pipe(fds) < 0)
+               return 0;
+
+       child = fork();
+       if (child < 0)
+               return 0;
+
+       if (child == 0) {
+               /* Child.  */
+               closefrom(STDERR_FILENO + 1);
+               _exit(0);
+       } else {
+               /* Parent.  */
+
+               /* Close write end of pipe.  */
+               close(fds[1]);
+
+               do {
+                       rres = read(fds[0], &buf, 1);
+               } while ((rres < 0) && (errno == EINTR));
+
+               /* Should have seen EOF.  */
+               if (rres != 0)
+                       return 0;
+
+               /* Clean up.  */
+               waitpid(child, NULL, 0);
+               closefrom(STDERR_FILENO + 1);
+       }
+
+       return 1;
+}
+/* Open a pipe, fork, in child set the write end to fd #3,
+ * in parent set the read end to fd #3, send a byte from
+ * child to parent, check.
+ */
+static
+int fork_communicate()
+{
+       int fds[2];
+       pid_t child;
+
+       char wbuf = 42;
+       char rbuf;
+       ssize_t rres;
+       ssize_t wres;
+
+       int status;
+
+       if (pipe(fds) < 0)
+               return 0;
+
+       child = fork();
+       if (child < 0)
+               return 0;
+
+       if (child == 0) {
+               /* Child.  */
+
+               /* Move write end to fd #3.  */
+               if (fds[1] != 3) {
+                       if (dup2(fds[1], 3) < 0)
+                               _exit(127);
+                       close(fds[1]);
+                       fds[1] = 3;
+               }
+
+               closefrom(4);
+
+               do {
+                       wres = write(fds[1], &wbuf, 1);
+               } while ((wres < 0) && (errno == EINTR));
+               if (wres < 0)
+                       _exit(127);
+
+               _exit(0);
+       } else {
+               /* Parent.  */
+
+               /* Move read end to fd #3.  */
+               if (fds[0] != 3) {
+                       if (dup2(fds[0], 3) < 0)
+                               return 0;
+                       close(fds[0]);
+                       fds[0] = 3;
+               }
+
+               closefrom(4);
+
+               /* Wait for child to finish.  */
+               waitpid(child, &status, 0);
+               if (!WIFEXITED(status))
+                       return 0;
+               if (WEXITSTATUS(status) != 0)
+                       return 0;
+
+               /* Read 1 byte.  */
+               do {
+                       rres = read(fds[0], &rbuf, 1);
+               } while ((rres < 0) && (errno == EINTR));
+               if (rres < 0)
+                       return 0;
+               if (rres != 1)
+                       return 0;
+               /* Should get same byte as what was sent.  */
+               if (rbuf != wbuf)
+                       return 0;
+
+               /* Next attempt to read should EOF.  */
+               do {
+                       rres = read(fds[0], &rbuf, 1);
+               } while ((rres < 0) && (errno == EINTR));
+               if (rres < 0)
+                       return 0;
+               /* Should EOF.  */
+               if (rres != 0)
+                       return 0;
+
+       }
+
+       /* Clean up.  */
+       close(fds[0]);
+       return 1;
+}
+
+int main(void)
+{
+       /* Limit closefrom.  */
+       closefrom_limit(0);
+
+       /* This is how many tests you plan to run */
+       plan_tests(3);
+
+       ok1(pipe_close());
+       ok1(fork_close());
+       ok1(fork_communicate());
+
+       /* This exits depending on whether all tests passed */
+       return exit_status();
+}
index c0598509b8c0fa677ea426ab11e8b2a674f12838..57779f29905b68d82e537e8c4f0c49c2d282ebec 100644 (file)
@@ -498,6 +498,43 @@ static const struct test base_tests[] = {
          "     return __builtin_cpu_supports(\"mmx\");\n"
          "}"
        },
+       { "HAVE_CLOSEFROM", "closefrom() offered by system",
+         "DEFINES_EVERYTHING", NULL, NULL,
+         "#include <stdlib.h>\n"
+         "#include <unistd.h>\n"
+         "int main(void) {\n"
+         "    closefrom(STDERR_FILENO + 1);\n"
+         "    return 0;\n"
+         "}\n"
+       },
+       { "HAVE_F_CLOSEM", "F_CLOSEM defined for fctnl.",
+         "DEFINES_EVERYTHING", NULL, NULL,
+         "#include <fcntl.h>\n"
+         "#include <unistd.h>\n"
+         "int main(void) {\n"
+         "    int res = fcntl(STDERR_FILENO + 1, F_CLOSEM, 0);\n"
+         "    return res < 0;\n"
+         "}\n"
+       },
+       { "HAVE_NR_CLOSE_RANGE", "close_range syscall available as __NR_close_range.",
+         "DEFINES_EVERYTHING", NULL, NULL,
+         "#include <limits.h>\n"
+         "#include <sys/syscall.h>\n"
+         "#include <unistd.h>\n"
+         "int main(void) {\n"
+         "    int res = syscall(__NR_close_range, STDERR_FILENO + 1, INT_MAX, 0);\n"
+         "    return res < 0;\n"
+         "}\n"
+       },
+       { "HAVE_F_MAXFD", "F_MAXFD defined for fcntl.",
+         "DEFINES_EVERYTHING", NULL, NULL,
+         "#include <fcntl.h>\n"
+         "#include <unistd.h>\n"
+         "int main(void) {\n"
+         "    int res = fcntl(0, F_MAXFD);\n"
+         "    return res < 0;\n"
+         "}\n"
+       },
 };
 
 static void c12r_err(int eval, const char *fmt, ...)