From ZmnSCPxj at protonmail.com Mon Oct 18 19:02:05 2021 From: ZmnSCPxj at protonmail.com (ZmnSCPxj) Date: Mon, 18 Oct 2021 08:02:05 -0000 Subject: [ccan] [PATCH 1/2] closefrom: Close all file descriptors above a certain value. Message-ID: >From 0ff501e58f71e1ce9bc296eb9efe01755d562947 Mon Sep 17 00:00:00 2001 From: ZmnSCPxj jxPCSnmZ Date: Mon, 18 Oct 2021 14:36:43 +0800 Subject: [PATCH 1/2] closefrom: Close all file descriptors above a certain value. For more information: https://github.com/ElementsProject/lightning/issues/4868 Signed-off-by: ZmnSCPxj jxPCSnmZ --- ccan/closefrom/LICENSE | 1 + ccan/closefrom/_info | 65 ++++++++++ ccan/closefrom/closefrom.c | 196 ++++++++++++++++++++++++++++++ ccan/closefrom/closefrom.h | 54 ++++++++ ccan/closefrom/test/run.c | 189 ++++++++++++++++++++++++++++ tools/configurator/configurator.c | 37 ++++++ 6 files changed, 542 insertions(+) create mode 120000 ccan/closefrom/LICENSE create mode 100644 ccan/closefrom/_info create mode 100644 ccan/closefrom/closefrom.c create mode 100644 ccan/closefrom/closefrom.h create mode 100644 ccan/closefrom/test/run.c diff --git a/ccan/closefrom/LICENSE b/ccan/closefrom/LICENSE new file mode 120000 index 00000000..b7951dab --- /dev/null +++ b/ccan/closefrom/LICENSE @@ -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 index 00000000..4e6f2f16 --- /dev/null +++ b/ccan/closefrom/_info @@ -0,0 +1,65 @@ +#include "config.h" +#include +#include + +/** + * 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 + * + * Example: + * #include + * #include + * #include + * #include + * #include + * #include + * #include + * + * int main(int argc, char **argv) + * { + * pid_t child; + * + * if (closefrom_may_be_slow()) { + * // 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_may_be_slow() will return + * // true and we should keep _SC_OPEN_MAX down + * // by the below: + * struct rlimit nofile = {1024, 1024}; + * setrlimit(RLIMIT_NOFILE, &nofile); + * } + * + * 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 index 00000000..b529f724 --- /dev/null +++ b/ccan/closefrom/closefrom.c @@ -0,0 +1,196 @@ +/* CC0 license (public domain) - see LICENSE file for details */ +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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 + +void closefrom(int fromfd) +{ + (void) fcntl(fromfd, F_CLOSEM, 0); +} + +int closefrom_may_be_slow(void) +{ + return 0; +} + +#else /* !HAVE_F_CLOSEM */ + +#if HAVE_NR_CLOSE_RANGE +#include +#endif + +#define PROC_PID_FD_LEN \ + ( 6 /* /proc/ */ \ + + 20 /* 64-bit $PID */ \ + + 3 /* /fd */ \ + + 1 /* NUL */ \ + ) + +static int can_get_maxfd(void) +{ +#if HAVE_F_MAXFD + int res = fcntl(0, F_MAXFD); + if (res < 0) + return 0; + else + return 1; +#else + return 0; +#endif +} + +/* Linux >= 5.9 */ +static int can_close_range(void) +{ +#if HAVE_NR_CLOSE_RANGE + int res = syscall(__NR_close_range, INT_MAX, INT_MAX, 0); + if (res < 0) + return 0; + return 1; +#else + return 0; +#endif +} + +/* On Linux, Solaris, AIX, Cygwin, and NetBSD. */ +static int 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 0; + closedir(dir); + return 1; +} + +/* On FreeBSD and MacOS. */ +static int can_open_dev_fd(void) +{ + DIR *dir; + dir = opendir("/dev/fd"); + if (!dir) + return 0; + closedir(dir); + return 1; +} + +int closefrom_may_be_slow(void) +{ + if (can_get_maxfd()) + return 0; + else if (can_close_range()) + return 0; + else if (can_open_proc_pid_fd()) + return 0; + else if (can_open_dev_fd()) + return 0; + else + return 1; +} + +/* 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 */ + +#endif /* !HAVE_CLOSEFROM */ diff --git a/ccan/closefrom/closefrom.h b/ccan/closefrom/closefrom.h new file mode 100644 index 00000000..e3270e6a --- /dev/null +++ b/ccan/closefrom/closefrom.h @@ -0,0 +1,54 @@ +/* CC0 license (public domain) - see LICENSE file for details */ +#ifndef CCAN_CLOSEFROM_H +#define CCAN_CLOSEFROM_H +#include "config.h" + +#if HAVE_CLOSEFROM +/* BSD. */ +#include +/* Solaris. */ +#include + +static inline +int closefrom_may_be_slow(void) +{ + return 0; +} + +#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 non-0 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 0, then closefrom is efficient and you do not + * need to limit the number of file descriptors. + */ +int closefrom_may_be_slow(void); + +#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 index 00000000..843fbdcd --- /dev/null +++ b/ccan/closefrom/test/run.c @@ -0,0 +1,189 @@ +#include +/* Include the C files directly. */ +#include +#include +#include +#include +#include +#include + +/* 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) +{ + /* 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(); +} diff --git a/tools/configurator/configurator.c b/tools/configurator/configurator.c index c0598509..fee9df83 100644 --- a/tools/configurator/configurator.c +++ b/tools/configurator/configurator.c @@ -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 \n" + "#include \n" + "int main(void) {\n" + " int res = closefrom(STDERR_FILENO + 1);\n" + " return res < 0;\n" + "}\n" + }, + { "HAVE_F_CLOSEM", "F_CLOSEM defined for fctnl.", + "DEFINES_EVERYTHING", NULL, NULL, + "#include \n" + "#include \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 \n" + "#include \n" + "#include \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 \n" + "#include \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, ...) -- 2.33.0 From ZmnSCPxj at protonmail.com Mon Oct 18 19:04:52 2021 From: ZmnSCPxj at protonmail.com (ZmnSCPxj) Date: Mon, 18 Oct 2021 08:04:52 -0000 Subject: [ccan] [PATCH 2/2] pipecmd: Use closefrom instead of iterating directly. Message-ID: >From b48091be33010782e0b82245f3518c7b4cc94b9c Mon Sep 17 00:00:00 2001 From: ZmnSCPxj jxPCSnmZ Date: Mon, 18 Oct 2021 15:50:31 +0800 Subject: [PATCH 2/2] pipecmd: Use closefrom instead of iterating directly. Signed-off-by: ZmnSCPxj jxPCSnmZ --- ccan/pipecmd/_info | 1 + ccan/pipecmd/pipecmd.c | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ccan/pipecmd/_info b/ccan/pipecmd/_info index 8c49511a..824f22e3 100644 --- a/ccan/pipecmd/_info +++ b/ccan/pipecmd/_info @@ -51,6 +51,7 @@ int main(int argc, char *argv[]) return 1; if (strcmp(argv[1], "depends") == 0) { + printf("ccan/closefrom\n"); printf("ccan/noerr\n"); return 0; } diff --git a/ccan/pipecmd/pipecmd.c b/ccan/pipecmd/pipecmd.c index 56cb67b6..0090275b 100644 --- a/ccan/pipecmd/pipecmd.c +++ b/ccan/pipecmd/pipecmd.c @@ -1,4 +1,5 @@ /* CC0 license (public domain) - see LICENSE file for details */ +#include #include #include #include @@ -139,11 +140,21 @@ pid_t pipecmdarr(int *fd_tochild, int *fd_fromchild, int *fd_errfromchild, close(errfromchild[1]); } + /* Map execfail[1] to fd 3. */ + if (execfail[1] != 3) { + if (dup2(execfail[1], 3) == -1) + goto child_errno_fail; + /* CLOEXEC is not shared by dup2, so copy the flags + * from execfail[1] to 3. + */ + if (fcntl(3, F_SETFD, fcntl(execfail[1], F_GETFD)) < 0) + goto child_errno_fail; + close(execfail[1]); + execfail[1] = 3; + } + /* Make (fairly!) sure all other fds are closed. */ - int max = sysconf(_SC_OPEN_MAX); - for (i = 3; i < max; i++) - if (i != execfail[1]) - close(i); + closefrom(4); execvp(arr[0], arr); -- 2.33.0