From 69c1a05ede969408aa30f3b49188bc621878f650 Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Mon, 5 Feb 2024 20:26:52 +0000 Subject: [PATCH 1/3] glib-unix: Add g_closefrom(), g_fdwalk_set_cloexec() These are the same as Linux `close_range (lowfd, ~0U, 0)` and `close_range (lowfd, ~0U, CLOSE_RANGE_CLOEXEC)`, but portable. Unlike some implementations of BSD closefrom(3), they are async-signal-safe. The implementations were moved from the GSpawn code, which already needs all of this functionality anyway, with the exception of set_cloexec() which was copied (leading to some minor duplication, but it's very simple). Resolves: https://gitlab.gnome.org/GNOME/glib/-/issues/3247 Signed-off-by: Simon McVittie --- glib/glib-unix.c | 421 ++++++++++++++++++++++++++++++++++++++++++++++- glib/glib-unix.h | 6 + glib/gspawn.c | 335 +------------------------------------ 3 files changed, 427 insertions(+), 335 deletions(-) diff --git a/glib/glib-unix.c b/glib/glib-unix.c index a7716fb7f..7cb76dd33 100644 --- a/glib/glib-unix.c +++ b/glib/glib-unix.c @@ -1,6 +1,20 @@ /* GLIB - Library of useful routines for C programming - * Copyright (C) 2011 Red Hat, Inc. - * Copyright 2023 Collabora Ltd. + * Copyright 2000-2022 Red Hat, Inc. + * Copyright 2006-2007 Matthias Clasen + * Copyright 2006 Padraig O'Briain + * Copyright 2007 Lennart Poettering + * Copyright 2018-2022 Endless OS Foundation, LLC + * Copyright 2018 Peter Wu + * Copyright 2019 Ting-Wei Lan + * Copyright 2019 Sebastian Schwarz + * Copyright 2020 Matt Rose + * Copyright 2021 Casper Dik + * Copyright 2022 Alexander Richardson + * Copyright 2022 Ray Strode + * Copyright 2022 Thomas Haller + * Copyright 2023-2024 Collabora Ltd. + * Copyright 2023 Sebastian Wilhelmi + * Copyright 2023 CaiJingLong * * glib-unix.c: UNIX specific API wrappers and convenience functions * @@ -28,9 +42,27 @@ #include "glib-unixprivate.h" #include "gmain-internal.h" +#include +#include +#include +#include /* for fdwalk */ #include #include #include +#include + +#if defined(__linux__) || defined(__DragonFly__) +#include /* for syscall and SYS_getdents64 */ +#endif + +#ifdef HAVE_SYS_RESOURCE_H +#include +#endif /* HAVE_SYS_RESOURCE_H */ + +#if defined(__APPLE__) && defined(HAVE_LIBPROC_H) +#include +#include +#endif G_STATIC_ASSERT (sizeof (ssize_t) == GLIB_SIZEOF_SSIZE_T); G_STATIC_ASSERT (G_ALIGNOF (gssize) == G_ALIGNOF (ssize_t)); @@ -507,3 +539,388 @@ g_unix_get_passwd_entry (const gchar *user_name, return (struct passwd *) g_steal_pointer (&buffer); } + +/* This function is called between fork() and exec() and hence must be + * async-signal-safe (see signal-safety(7)). */ +static int +set_cloexec (void *data, gint fd) +{ + if (fd >= GPOINTER_TO_INT (data)) + fcntl (fd, F_SETFD, FD_CLOEXEC); + + return 0; +} + +/* fdwalk()-compatible callback to close a fd for non-compliant + * implementations of fdwalk() that potentially pass already + * closed fds. + * + * It is not an error to pass an invalid fd to this function. + * + * This function is called between fork() and exec() and hence must be + * async-signal-safe (see signal-safety(7)). + */ +G_GNUC_UNUSED static int +close_func_with_invalid_fds (void *data, int fd) +{ + /* We use close and not g_close here because on some platforms, we + * don't know how to close only valid, open file descriptors, so we + * have to pass bad fds to close too. g_close warns if given a bad + * fd. + * + * This function returns no error, because there is nothing that the caller + * could do with that information. That is even the case for EINTR. See + * g_close() about the specialty of EINTR and why that is correct. + * If g_close() ever gets extended to handle EINTR specially, then this place + * should get updated to do the same handling. + */ + if (fd >= GPOINTER_TO_INT (data)) + close (fd); + + return 0; +} + +#ifdef __linux__ +struct linux_dirent64 +{ + guint64 d_ino; /* 64-bit inode number */ + guint64 d_off; /* 64-bit offset to next structure */ + unsigned short d_reclen; /* Size of this dirent */ + unsigned char d_type; /* File type */ + char d_name[]; /* Filename (null-terminated) */ +}; + +/* This function is called between fork() and exec() and hence must be + * async-signal-safe (see signal-safety(7)). */ +static gint +filename_to_fd (const char *p) +{ + char c; + int fd = 0; + const int cutoff = G_MAXINT / 10; + const int cutlim = G_MAXINT % 10; + + if (*p == '\0') + return -1; + + while ((c = *p++) != '\0') + { + if (c < '0' || c > '9') + return -1; + c -= '0'; + + /* Check for overflow. */ + if (fd > cutoff || (fd == cutoff && c > cutlim)) + return -1; + + fd = fd * 10 + c; + } + + return fd; +} +#endif + +static int safe_fdwalk_with_invalid_fds (int (*cb)(void *data, int fd), void *data); + +/* This function is called between fork() and exec() and hence must be + * async-signal-safe (see signal-safety(7)). */ +static int +safe_fdwalk (int (*cb)(void *data, int fd), void *data) +{ +#if 0 + /* Use fdwalk function provided by the system if it is known to be + * async-signal safe. + * + * Currently there are no operating systems known to provide a safe + * implementation, so this section is not used for now. + */ + return fdwalk (cb, data); +#else + /* Fallback implementation of fdwalk. It should be async-signal safe, but it + * may fail on non-Linux operating systems. See safe_fdwalk_with_invalid_fds + * for a slower alternative. + */ + +#ifdef __linux__ + gint fd; + gint res = 0; + + /* Avoid use of opendir/closedir since these are not async-signal-safe. */ + int dir_fd = open ("/proc/self/fd", O_RDONLY | O_DIRECTORY); + if (dir_fd >= 0) + { + /* buf needs to be aligned correctly to receive linux_dirent64. + * C11 has _Alignof for this purpose, but for now a + * union serves the same purpose. */ + union + { + char buf[4096]; + struct linux_dirent64 alignment; + } u; + int pos, nread; + struct linux_dirent64 *de; + + while ((nread = syscall (SYS_getdents64, dir_fd, u.buf, sizeof (u.buf))) > 0) + { + for (pos = 0; pos < nread; pos += de->d_reclen) + { + de = (struct linux_dirent64 *) (u.buf + pos); + + fd = filename_to_fd (de->d_name); + if (fd < 0 || fd == dir_fd) + continue; + + if ((res = cb (data, fd)) != 0) + break; + } + } + + g_close (dir_fd, NULL); + return res; + } + + /* If /proc is not mounted or not accessible we fail here and rely on + * safe_fdwalk_with_invalid_fds to fall back to the old + * rlimit trick. */ + +#endif + +#if defined(__sun__) && defined(F_PREVFD) && defined(F_NEXTFD) +/* + * Solaris 11.4 has a signal-safe way which allows + * us to find all file descriptors in a process. + * + * fcntl(fd, F_NEXTFD, maxfd) + * - returns the first allocated file descriptor <= maxfd > fd. + * + * fcntl(fd, F_PREVFD) + * - return highest allocated file descriptor < fd. + */ + gint fd; + gint res = 0; + + open_max = fcntl (INT_MAX, F_PREVFD); /* find the maximum fd */ + if (open_max < 0) /* No open files */ + return 0; + + for (fd = -1; (fd = fcntl (fd, F_NEXTFD, open_max)) != -1; ) + if ((res = cb (data, fd)) != 0 || fd == open_max) + break; + + return res; +#endif + + return safe_fdwalk_with_invalid_fds (cb, data); +#endif +} + +/* This function is called between fork() and exec() and hence must be + * async-signal-safe (see signal-safety(7)). */ +static int +safe_fdwalk_with_invalid_fds (int (*cb)(void *data, int fd), void *data) +{ + /* Fallback implementation of fdwalk. It should be async-signal safe, but it + * may be slow, especially on systems allowing very high number of open file + * descriptors. + */ + gint open_max = -1; + gint fd; + gint res = 0; + +#if 0 && defined(HAVE_SYS_RESOURCE_H) + struct rlimit rl; + + /* Use getrlimit() function provided by the system if it is known to be + * async-signal safe. + * + * Currently there are no operating systems known to provide a safe + * implementation, so this section is not used for now. + */ + if (getrlimit (RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY) + open_max = rl.rlim_max; +#endif +#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__APPLE__) + /* Use sysconf() function provided by the system if it is known to be + * async-signal safe. + * + * FreeBSD: sysconf() is included in the list of async-signal safe functions + * found in https://man.freebsd.org/sigaction(2). + * + * OpenBSD: sysconf() is included in the list of async-signal safe functions + * found in https://man.openbsd.org/sigaction.2. + * + * Apple: sysconf() is included in the list of async-signal safe functions + * found in https://opensource.apple.com/source/xnu/xnu-517.12.7/bsd/man/man2/sigaction.2 + */ + if (open_max < 0) + open_max = sysconf (_SC_OPEN_MAX); +#endif + /* Hardcoded fallback: the default process hard limit in Linux as of 2020 */ + if (open_max < 0) + open_max = 4096; + +#if defined(__APPLE__) && defined(HAVE_LIBPROC_H) + /* proc_pidinfo isn't documented as async-signal-safe but looking at the implementation + * in the darwin tree here: + * + * https://opensource.apple.com/source/Libc/Libc-498/darwin/libproc.c.auto.html + * + * It's just a thin wrapper around a syscall, so it's probably okay. + */ + { + char buffer[4096 * PROC_PIDLISTFD_SIZE]; + ssize_t buffer_size; + + buffer_size = proc_pidinfo (getpid (), PROC_PIDLISTFDS, 0, buffer, sizeof (buffer)); + + if (buffer_size > 0 && + sizeof (buffer) >= (size_t) buffer_size && + (buffer_size % PROC_PIDLISTFD_SIZE) == 0) + { + const struct proc_fdinfo *fd_info = (const struct proc_fdinfo *) buffer; + size_t number_of_fds = (size_t) buffer_size / PROC_PIDLISTFD_SIZE; + + for (size_t i = 0; i < number_of_fds; i++) + if ((res = cb (data, fd_info[i].proc_fd)) != 0) + break; + + return res; + } + } +#endif + + for (fd = 0; fd < open_max; fd++) + if ((res = cb (data, fd)) != 0) + break; + + return res; +} + +/** + * g_fdwalk_set_cloexec: + * @lowfd: Minimum fd to act on, which must be non-negative + * + * Mark every file descriptor equal to or greater than @lowfd to be closed + * at the next `execve()` or similar, as if via the `FD_CLOEXEC` flag. + * + * Typically @lowfd will be 3, to leave standard input, standard output + * and standard error open after exec. + * + * This is the same as Linux `close_range (lowfd, ~0U, CLOSE_RANGE_CLOEXEC)`, + * but portable to other OSs and to older versions of Linux. + * + * This function is async-signal safe, making it safe to call from a + * signal handler or a [callback@GLib.SpawnChildSetupFunc], as long as @lowfd is + * non-negative. + * See [`signal(7)`](man:signal(7)) and + * [`signal-safety(7)`](man:signal-safety(7)) for more details. + * + * Returns: 0 on success, -1 with errno set on error + * Since: 2.80 + */ +int +g_fdwalk_set_cloexec (int lowfd) +{ + int ret; + + g_return_val_if_fail (lowfd >= 0, (errno = EINVAL, -1)); + +#if defined(HAVE_CLOSE_RANGE) && defined(CLOSE_RANGE_CLOEXEC) + /* close_range() is available in Linux since kernel 5.9, and on FreeBSD at + * around the same time. It was designed for use in async-signal-safe + * situations: https://bugs.python.org/issue38061 + * + * The `CLOSE_RANGE_CLOEXEC` flag was added in Linux 5.11, and is not yet + * present in FreeBSD. + * + * Handle ENOSYS in case it’s supported in libc but not the kernel; if so, + * fall back to safe_fdwalk(). Handle EINVAL in case `CLOSE_RANGE_CLOEXEC` + * is not supported. */ + ret = close_range (lowfd, G_MAXUINT, CLOSE_RANGE_CLOEXEC); + if (ret == 0 || !(errno == ENOSYS || errno == EINVAL)) + return ret; +#endif /* HAVE_CLOSE_RANGE */ + + ret = safe_fdwalk (set_cloexec, GINT_TO_POINTER (lowfd)); + + return ret; +} + +/** + * g_closefrom: + * @lowfd: Minimum fd to close, which must be non-negative + * + * Close every file descriptor equal to or greater than @lowfd. + * + * Typically @lowfd will be 3, to leave standard input, standard output + * and standard error open. + * + * This is the same as Linux `close_range (lowfd, ~0U, 0)`, + * but portable to other OSs and to older versions of Linux. + * Equivalently, it is the same as BSD `closefrom (lowfd)`, but portable, + * and async-signal-safe on all OSs. + * + * This function is async-signal safe, making it safe to call from a + * signal handler or a [callback@GLib.SpawnChildSetupFunc], as long as @lowfd is + * non-negative. + * See [`signal(7)`](man:signal(7)) and + * [`signal-safety(7)`](man:signal-safety(7)) for more details. + * + * Returns: 0 on success, -1 with errno set on error + * Since: 2.80 + */ +int +g_closefrom (int lowfd) +{ + int ret; + + g_return_val_if_fail (lowfd >= 0, (errno = EINVAL, -1)); + +#if defined(HAVE_CLOSE_RANGE) + /* close_range() is available in Linux since kernel 5.9, and on FreeBSD at + * around the same time. It was designed for use in async-signal-safe + * situations: https://bugs.python.org/issue38061 + * + * Handle ENOSYS in case it’s supported in libc but not the kernel; if so, + * fall back to safe_fdwalk(). */ + ret = close_range (lowfd, G_MAXUINT, 0); + if (ret == 0 || errno != ENOSYS) + return ret; +#endif /* HAVE_CLOSE_RANGE */ + +#if defined(__FreeBSD__) || defined(__OpenBSD__) || \ + (defined(__sun__) && defined(F_CLOSEFROM)) + /* Use closefrom function provided by the system if it is known to be + * async-signal safe. + * + * FreeBSD: closefrom is included in the list of async-signal safe functions + * found in https://man.freebsd.org/sigaction(2). + * + * OpenBSD: closefrom is not included in the list, but a direct system call + * should be safe to use. + * + * In Solaris as of 11.3 SRU 31, closefrom() is also a direct system call. + * On such systems, F_CLOSEFROM is defined. + */ + (void) closefrom (lowfd); + return 0; +#elif defined(__DragonFly__) + /* It is unclear whether closefrom function included in DragonFlyBSD libc_r + * is safe to use because it calls a lot of library functions. It is also + * unclear whether libc_r itself is still being used. Therefore, we do a + * direct system call here ourselves to avoid possible issues. + */ + (void) syscall (SYS_closefrom, lowfd); + return 0; +#elif defined(F_CLOSEM) + /* NetBSD and AIX have a special fcntl command which does the same thing as + * closefrom. NetBSD also includes closefrom function, which seems to be a + * simple wrapper of the fcntl command. + */ + return fcntl (lowfd, F_CLOSEM); +#else + ret = safe_fdwalk (close_func_with_invalid_fds, GINT_TO_POINTER (lowfd)); + + return ret; +#endif +} diff --git a/glib/glib-unix.h b/glib/glib-unix.h index c58769ec2..f6e9e7eed 100644 --- a/glib/glib-unix.h +++ b/glib/glib-unix.h @@ -326,6 +326,12 @@ g_unix_pipe_clear (GUnixPipe *self) G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (GUnixPipe, g_unix_pipe_clear) +GLIB_AVAILABLE_IN_2_80 +int g_closefrom (int lowfd); + +GLIB_AVAILABLE_IN_2_80 +int g_fdwalk_set_cloexec (int lowfd); + G_GNUC_END_IGNORE_DEPRECATIONS G_END_DECLS diff --git a/glib/gspawn.c b/glib/gspawn.c index 0720cc3b7..e434d8f84 100644 --- a/glib/gspawn.c +++ b/glib/gspawn.c @@ -1265,337 +1265,6 @@ dupfd_cloexec (int old_fd, int new_fd_min) return fd; } -/* fdwalk()-compatible callback to close a fd for non-compliant - * implementations of fdwalk() that potentially pass already - * closed fds. - * - * It is not an error to pass an invalid fd to this function. - * - * This function is called between fork() and exec() and hence must be - * async-signal-safe (see signal-safety(7)). - */ -G_GNUC_UNUSED static int -close_func_with_invalid_fds (void *data, int fd) -{ - /* We use close and not g_close here because on some platforms, we - * don't know how to close only valid, open file descriptors, so we - * have to pass bad fds to close too. g_close warns if given a bad - * fd. - * - * This function returns no error, because there is nothing that the caller - * could do with that information. That is even the case for EINTR. See - * g_close() about the specialty of EINTR and why that is correct. - * If g_close() ever gets extended to handle EINTR specially, then this place - * should get updated to do the same handling. - */ - if (fd >= GPOINTER_TO_INT (data)) - close (fd); - - return 0; -} - -#ifdef __linux__ -struct linux_dirent64 -{ - guint64 d_ino; /* 64-bit inode number */ - guint64 d_off; /* 64-bit offset to next structure */ - unsigned short d_reclen; /* Size of this dirent */ - unsigned char d_type; /* File type */ - char d_name[]; /* Filename (null-terminated) */ -}; - -/* This function is called between fork() and exec() and hence must be - * async-signal-safe (see signal-safety(7)). */ -static gint -filename_to_fd (const char *p) -{ - char c; - int fd = 0; - const int cutoff = G_MAXINT / 10; - const int cutlim = G_MAXINT % 10; - - if (*p == '\0') - return -1; - - while ((c = *p++) != '\0') - { - if (c < '0' || c > '9') - return -1; - c -= '0'; - - /* Check for overflow. */ - if (fd > cutoff || (fd == cutoff && c > cutlim)) - return -1; - - fd = fd * 10 + c; - } - - return fd; -} -#endif - -static int safe_fdwalk_with_invalid_fds (int (*cb)(void *data, int fd), void *data); - -/* This function is called between fork() and exec() and hence must be - * async-signal-safe (see signal-safety(7)). */ -static int -safe_fdwalk (int (*cb)(void *data, int fd), void *data) -{ -#if 0 - /* Use fdwalk function provided by the system if it is known to be - * async-signal safe. - * - * Currently there are no operating systems known to provide a safe - * implementation, so this section is not used for now. - */ - return fdwalk (cb, data); -#else - /* Fallback implementation of fdwalk. It should be async-signal safe, but it - * may fail on non-Linux operating systems. See safe_fdwalk_with_invalid_fds - * for a slower alternative. - */ - -#ifdef __linux__ - gint fd; - gint res = 0; - - /* Avoid use of opendir/closedir since these are not async-signal-safe. */ - int dir_fd = open ("/proc/self/fd", O_RDONLY | O_DIRECTORY); - if (dir_fd >= 0) - { - /* buf needs to be aligned correctly to receive linux_dirent64. - * C11 has _Alignof for this purpose, but for now a - * union serves the same purpose. */ - union - { - char buf[4096]; - struct linux_dirent64 alignment; - } u; - int pos, nread; - struct linux_dirent64 *de; - - while ((nread = syscall (SYS_getdents64, dir_fd, u.buf, sizeof (u.buf))) > 0) - { - for (pos = 0; pos < nread; pos += de->d_reclen) - { - de = (struct linux_dirent64 *) (u.buf + pos); - - fd = filename_to_fd (de->d_name); - if (fd < 0 || fd == dir_fd) - continue; - - if ((res = cb (data, fd)) != 0) - break; - } - } - - g_close (dir_fd, NULL); - return res; - } - - /* If /proc is not mounted or not accessible we fail here and rely on - * safe_fdwalk_with_invalid_fds to fall back to the old - * rlimit trick. */ - -#endif - -#if defined(__sun__) && defined(F_PREVFD) && defined(F_NEXTFD) -/* - * Solaris 11.4 has a signal-safe way which allows - * us to find all file descriptors in a process. - * - * fcntl(fd, F_NEXTFD, maxfd) - * - returns the first allocated file descriptor <= maxfd > fd. - * - * fcntl(fd, F_PREVFD) - * - return highest allocated file descriptor < fd. - */ - gint fd; - gint res = 0; - - open_max = fcntl (INT_MAX, F_PREVFD); /* find the maximum fd */ - if (open_max < 0) /* No open files */ - return 0; - - for (fd = -1; (fd = fcntl (fd, F_NEXTFD, open_max)) != -1; ) - if ((res = cb (data, fd)) != 0 || fd == open_max) - break; - - return res; -#endif - - return safe_fdwalk_with_invalid_fds (cb, data); -#endif -} - -/* This function is called between fork() and exec() and hence must be - * async-signal-safe (see signal-safety(7)). */ -static int -safe_fdwalk_with_invalid_fds (int (*cb)(void *data, int fd), void *data) -{ - /* Fallback implementation of fdwalk. It should be async-signal safe, but it - * may be slow, especially on systems allowing very high number of open file - * descriptors. - */ - gint open_max = -1; - gint fd; - gint res = 0; - -#if 0 && defined(HAVE_SYS_RESOURCE_H) - struct rlimit rl; - - /* Use getrlimit() function provided by the system if it is known to be - * async-signal safe. - * - * Currently there are no operating systems known to provide a safe - * implementation, so this section is not used for now. - */ - if (getrlimit (RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY) - open_max = rl.rlim_max; -#endif -#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__APPLE__) - /* Use sysconf() function provided by the system if it is known to be - * async-signal safe. - * - * FreeBSD: sysconf() is included in the list of async-signal safe functions - * found in https://man.freebsd.org/sigaction(2). - * - * OpenBSD: sysconf() is included in the list of async-signal safe functions - * found in https://man.openbsd.org/sigaction.2. - * - * Apple: sysconf() is included in the list of async-signal safe functions - * found in https://opensource.apple.com/source/xnu/xnu-517.12.7/bsd/man/man2/sigaction.2 - */ - if (open_max < 0) - open_max = sysconf (_SC_OPEN_MAX); -#endif - /* Hardcoded fallback: the default process hard limit in Linux as of 2020 */ - if (open_max < 0) - open_max = 4096; - -#if defined(__APPLE__) && defined(HAVE_LIBPROC_H) - /* proc_pidinfo isn't documented as async-signal-safe but looking at the implementation - * in the darwin tree here: - * - * https://opensource.apple.com/source/Libc/Libc-498/darwin/libproc.c.auto.html - * - * It's just a thin wrapper around a syscall, so it's probably okay. - */ - { - char buffer[4096 * PROC_PIDLISTFD_SIZE]; - ssize_t buffer_size; - - buffer_size = proc_pidinfo (getpid (), PROC_PIDLISTFDS, 0, buffer, sizeof (buffer)); - - if (buffer_size > 0 && - sizeof (buffer) >= (size_t) buffer_size && - (buffer_size % PROC_PIDLISTFD_SIZE) == 0) - { - const struct proc_fdinfo *fd_info = (const struct proc_fdinfo *) buffer; - size_t number_of_fds = (size_t) buffer_size / PROC_PIDLISTFD_SIZE; - - for (size_t i = 0; i < number_of_fds; i++) - if ((res = cb (data, fd_info[i].proc_fd)) != 0) - break; - - return res; - } - } -#endif - - for (fd = 0; fd < open_max; fd++) - if ((res = cb (data, fd)) != 0) - break; - - return res; -} - -/* This function is called between fork() and exec() and hence must be - * async-signal-safe (see signal-safety(7)). */ -static int -safe_fdwalk_set_cloexec (int lowfd) -{ - int ret; - -#if defined(HAVE_CLOSE_RANGE) && defined(CLOSE_RANGE_CLOEXEC) - /* close_range() is available in Linux since kernel 5.9, and on FreeBSD at - * around the same time. It was designed for use in async-signal-safe - * situations: https://bugs.python.org/issue38061 - * - * The `CLOSE_RANGE_CLOEXEC` flag was added in Linux 5.11, and is not yet - * present in FreeBSD. - * - * Handle ENOSYS in case it’s supported in libc but not the kernel; if so, - * fall back to safe_fdwalk(). Handle EINVAL in case `CLOSE_RANGE_CLOEXEC` - * is not supported. */ - ret = close_range (lowfd, G_MAXUINT, CLOSE_RANGE_CLOEXEC); - if (ret == 0 || !(errno == ENOSYS || errno == EINVAL)) - return ret; -#endif /* HAVE_CLOSE_RANGE */ - - ret = safe_fdwalk (set_cloexec, GINT_TO_POINTER (lowfd)); - - return ret; -} - -/* This function is called between fork() and exec() and hence must be - * async-signal-safe (see signal-safety(7)). - * - * On failure, `-1` will be returned and errno will be set. */ -static int -safe_closefrom (int lowfd) -{ - int ret; - -#if defined(HAVE_CLOSE_RANGE) - /* close_range() is available in Linux since kernel 5.9, and on FreeBSD at - * around the same time. It was designed for use in async-signal-safe - * situations: https://bugs.python.org/issue38061 - * - * Handle ENOSYS in case it’s supported in libc but not the kernel; if so, - * fall back to safe_fdwalk(). */ - ret = close_range (lowfd, G_MAXUINT, 0); - if (ret == 0 || errno != ENOSYS) - return ret; -#endif /* HAVE_CLOSE_RANGE */ - -#if defined(__FreeBSD__) || defined(__OpenBSD__) || \ - (defined(__sun__) && defined(F_CLOSEFROM)) - /* Use closefrom function provided by the system if it is known to be - * async-signal safe. - * - * FreeBSD: closefrom is included in the list of async-signal safe functions - * found in https://man.freebsd.org/sigaction(2). - * - * OpenBSD: closefrom is not included in the list, but a direct system call - * should be safe to use. - * - * In Solaris as of 11.3 SRU 31, closefrom() is also a direct system call. - * On such systems, F_CLOSEFROM is defined. - */ - (void) closefrom (lowfd); - return 0; -#elif defined(__DragonFly__) - /* It is unclear whether closefrom function included in DragonFlyBSD libc_r - * is safe to use because it calls a lot of library functions. It is also - * unclear whether libc_r itself is still being used. Therefore, we do a - * direct system call here ourselves to avoid possible issues. - */ - (void) syscall (SYS_closefrom, lowfd); - return 0; -#elif defined(F_CLOSEM) - /* NetBSD and AIX have a special fcntl command which does the same thing as - * closefrom. NetBSD also includes closefrom function, which seems to be a - * simple wrapper of the fcntl command. - */ - return fcntl (lowfd, F_CLOSEM); -#else - ret = safe_fdwalk (close_func_with_invalid_fds, GINT_TO_POINTER (lowfd)); - - return ret; -#endif -} - /* This function is called between fork() and exec() and hence must be * async-signal-safe (see signal-safety(7)). */ static gint @@ -1823,13 +1492,13 @@ do_exec (gint child_err_report_fd, if (safe_dup2 (child_err_report_fd, 3) < 0) write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED); set_cloexec (GINT_TO_POINTER (0), 3); - if (safe_closefrom (4) < 0) + if (g_closefrom (4) < 0) write_err_and_exit (child_err_report_fd, CHILD_CLOSE_FAILED); child_err_report_fd = 3; } else { - if (safe_fdwalk_set_cloexec (3) < 0) + if (g_fdwalk_set_cloexec (3) < 0) write_err_and_exit (child_err_report_fd, CHILD_CLOSE_FAILED); } } From a7d5a6b69a131c0d40cc9d37e4b127c0c55073dd Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Mon, 5 Feb 2024 20:29:26 +0000 Subject: [PATCH 2/3] gspawn: Simplify set_cloexec The copy of this function that moved to glib-unix.c still needs to implement the fdwalk-style interface, but this copy does not, allowing us to turn it into a very simple syscall wrapper. Its remaining callers never check for errors, and it never failed anyway, so remove the returned value. Signed-off-by: Simon McVittie --- glib/gspawn.c | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/glib/gspawn.c b/glib/gspawn.c index e434d8f84..0ddd53249 100644 --- a/glib/gspawn.c +++ b/glib/gspawn.c @@ -1194,13 +1194,10 @@ write_err_and_exit (gint fd, gint msg) /* This function is called between fork() and exec() and hence must be * async-signal-safe (see signal-safety(7)). */ -static int -set_cloexec (void *data, gint fd) +static void +set_cloexec (int fd) { - if (fd >= GPOINTER_TO_INT (data)) - fcntl (fd, F_SETFD, FD_CLOEXEC); - - return 0; + fcntl (fd, F_SETFD, FD_CLOEXEC); } /* This function is called between fork() and exec() and hence must be @@ -1396,7 +1393,7 @@ do_exec (gint child_err_report_fd, write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED); - set_cloexec (GINT_TO_POINTER(0), stdin_fd); + set_cloexec (stdin_fd); } else if (!child_inherits_stdin) { @@ -1434,7 +1431,7 @@ do_exec (gint child_err_report_fd, write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED); - set_cloexec (GINT_TO_POINTER(0), stdout_fd); + set_cloexec (stdout_fd); } else if (stdout_to_null) { @@ -1466,7 +1463,7 @@ do_exec (gint child_err_report_fd, write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED); - set_cloexec (GINT_TO_POINTER(0), stderr_fd); + set_cloexec (stderr_fd); } else if (stderr_to_null) { @@ -1491,7 +1488,7 @@ do_exec (gint child_err_report_fd, { if (safe_dup2 (child_err_report_fd, 3) < 0) write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED); - set_cloexec (GINT_TO_POINTER (0), 3); + set_cloexec (3); if (g_closefrom (4) < 0) write_err_and_exit (child_err_report_fd, CHILD_CLOSE_FAILED); child_err_report_fd = 3; @@ -1505,7 +1502,7 @@ do_exec (gint child_err_report_fd, else { /* We need to do child_err_report_fd anyway */ - set_cloexec (GINT_TO_POINTER (0), child_err_report_fd); + set_cloexec (child_err_report_fd); } /* From f4afed90e605074e86900da9e9ee065cec91aa5b Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Mon, 5 Feb 2024 20:29:47 +0000 Subject: [PATCH 3/3] tests: Exercise g_fdwalk_set_cloexec() and g_closefrom() Signed-off-by: Simon McVittie --- glib/tests/unix.c | 184 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/glib/tests/unix.c b/glib/tests/unix.c index e4003f730..8f624c527 100644 --- a/glib/tests/unix.c +++ b/glib/tests/unix.c @@ -36,6 +36,187 @@ #include "testutils.h" +static void +async_signal_safe_message (const char *message) +{ + if (write (2, message, strlen (message)) < 0 || + write (2, "\n", 1) < 0) + { + /* ignore: not much we can do */ + } +} + +static void +test_closefrom (void) +{ + /* Enough file descriptors to be confident that we're operating on + * all of them */ + const int N_FDS = 20; + int *fds; + int fd; + int i; + pid_t child; + int wait_status; + + /* The loop that populates @fds with pipes assumes this */ + g_assert (N_FDS % 2 == 0); + + g_test_summary ("Test g_closefrom(), g_fdwalk_set_cloexec()"); + g_test_bug ("https://gitlab.gnome.org/GNOME/glib/-/issues/3247"); + + for (fd = 0; fd <= 2; fd++) + { + int flags; + + g_assert_no_errno ((flags = fcntl (fd, F_GETFD))); + g_assert_no_errno (fcntl (fd, F_SETFD, flags & ~FD_CLOEXEC)); + } + + fds = g_new0 (int, N_FDS); + + for (i = 0; i < N_FDS; i += 2) + { + GError *error = NULL; + int pipefd[2]; + int res; + + /* Intentionally neither O_CLOEXEC nor FD_CLOEXEC */ + res = g_unix_open_pipe (pipefd, 0, &error); + g_assert (res); + g_assert_no_error (error); + g_clear_error (&error); + fds[i] = pipefd[0]; + fds[i + 1] = pipefd[1]; + } + + child = fork (); + + /* Child process exits with status = 100 + the first wrong fd, + * or 0 if all were correct */ + if (child == 0) + { + for (i = 0; i < N_FDS; i++) + { + int flags = fcntl (fds[i], F_GETFD); + + if (flags == -1) + { + async_signal_safe_message ("fd should not have been closed"); + _exit (100 + fds[i]); + } + + if (flags & FD_CLOEXEC) + { + async_signal_safe_message ("fd should not have been close-on-exec yet"); + _exit (100 + fds[i]); + } + } + + g_fdwalk_set_cloexec (3); + + for (i = 0; i < N_FDS; i++) + { + int flags = fcntl (fds[i], F_GETFD); + + if (flags == -1) + { + async_signal_safe_message ("fd should not have been closed"); + _exit (100 + fds[i]); + } + + if (!(flags & FD_CLOEXEC)) + { + async_signal_safe_message ("fd should have been close-on-exec"); + _exit (100 + fds[i]); + } + } + + g_closefrom (3); + + for (fd = 0; fd <= 2; fd++) + { + int flags = fcntl (fd, F_GETFD); + + if (flags == -1) + { + async_signal_safe_message ("fd should not have been closed"); + _exit (100 + fd); + } + + if (flags & FD_CLOEXEC) + { + async_signal_safe_message ("fd should not have been close-on-exec"); + _exit (100 + fd); + } + } + + for (i = 0; i < N_FDS; i++) + { + if (fcntl (fds[i], F_GETFD) != -1 || errno != EBADF) + { + async_signal_safe_message ("fd should have been closed"); + _exit (100 + fds[i]); + } + } + + _exit (0); + } + + g_assert_no_errno (waitpid (child, &wait_status, 0)); + + if (WIFEXITED (wait_status)) + { + int exit_status = WEXITSTATUS (wait_status); + + if (exit_status != 0) + g_test_fail_printf ("File descriptor %d in incorrect state", exit_status - 100); + } + else + { + g_test_fail_printf ("Unexpected wait status %d", wait_status); + } + + for (i = 0; i < N_FDS; i++) + { + GError *error = NULL; + + g_close (fds[i], &error); + g_assert_no_error (error); + g_clear_error (&error); + } + + g_free (fds); + + if (g_test_undefined ()) + { + g_test_trap_subprocess ("/glib-unix/closefrom/subprocess/einval", + 0, G_TEST_SUBPROCESS_DEFAULT); + g_test_trap_assert_passed (); + } +} + +static void +test_closefrom_subprocess_einval (void) +{ + int res; + int errsv; + + g_log_set_always_fatal (G_LOG_FATAL_MASK); + g_log_set_fatal_mask ("GLib", G_LOG_FATAL_MASK); + + errno = 0; + res = g_closefrom (-1); + errsv = errno; + g_assert_cmpint (res, ==, -1); + g_assert_cmpint (errsv, ==, EINVAL); + + errno = 0; + res = g_fdwalk_set_cloexec (-42); + errsv = errno; + g_assert_cmpint (res, ==, -1); + g_assert_cmpint (errsv, ==, EINVAL); +} + static void test_pipe (void) { @@ -613,6 +794,9 @@ main (int argc, { g_test_init (&argc, &argv, NULL); + g_test_add_func ("/glib-unix/closefrom", test_closefrom); + g_test_add_func ("/glib-unix/closefrom/subprocess/einval", + test_closefrom_subprocess_einval); g_test_add_func ("/glib-unix/pipe", test_pipe); g_test_add_func ("/glib-unix/pipe/fd-cloexec", test_pipe_fd_cloexec); g_test_add_func ("/glib-unix/pipe-stdio-overwrite", test_pipe_stdio_overwrite);