diff --git a/glnx-backports.c b/glnx-backports.c index 391c154b..8b6bc4f0 100644 --- a/glnx-backports.c +++ b/glnx-backports.c @@ -1,12 +1,27 @@ /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * + * Copyright 2000-2022 Red Hat, Inc. + * Copyright 2006-2007 Matthias Clasen + * Copyright 2006 Padraig O'Briain + * Copyright 2007 Lennart Poettering * Copyright (C) 2015 Colin Walters - * Copyright (C) 2018 Endless OS Foundation, LLC - * SPDX-License-Identifier: LGPL-2.0-or-later + * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later * - * This program is free software: you can redistribute it and/or modify + * This library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published - * by the Free Software Foundation; either version 2 of the licence or (at + * by the Free Software Foundation; either version 2.1 of the licence or (at * your option) any later version. * * This library is distributed in the hope that it will be useful, @@ -14,16 +29,23 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * - * You should have received a copy of the GNU Lesser General - * Public License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place, Suite 330, - * Boston, MA 02111-1307, USA. + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . */ #include "libglnx-config.h" #include "glnx-backports.h" +#include +#include +#include +#include +#include +#include +#include +#include + #if !GLIB_CHECK_VERSION(2, 44, 0) gboolean glnx_strv_contains (const gchar * const *strv, @@ -82,3 +104,390 @@ _glnx_strv_equal (const gchar * const *strv1, return (*strv1 == NULL && *strv2 == NULL); } #endif + +#if !GLIB_CHECK_VERSION(2, 80, 0) +/* 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; + } + } + + close (dir_fd); + 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 +_glnx_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 +_glnx_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 +} +#endif /* !2.80.0 */ diff --git a/glnx-backports.h b/glnx-backports.h index 121e0731..7fa6a920 100644 --- a/glnx-backports.h +++ b/glnx-backports.h @@ -149,4 +149,11 @@ _glnx_steal_fd (int *fdp) #define G_TYPE_FLAG_NONE ((GTypeFlags) 0) #endif +#if !GLIB_CHECK_VERSION(2, 80, 0) +#define g_closefrom _glnx_closefrom +int _glnx_closefrom (int lowfd); +#define g_fdwalk_set_cloexec _glnx_fdwalk_set_cloexec +int _glnx_fdwalk_set_cloexec (int lowfd); +#endif + G_END_DECLS diff --git a/meson.build b/meson.build index a163e994..70c938f9 100644 --- a/meson.build +++ b/meson.build @@ -54,6 +54,16 @@ config_h = configure_file( configuration : conf, ) +check_functions = [ + 'close_range', +] +foreach check_function : check_functions +endforeach +config_h = configure_file( + output : 'libglnx-config.h', + configuration : conf, +) + libglnx_deps = [ dependency('gio-2.0'), dependency('gio-unix-2.0'), diff --git a/tests/test-libglnx-backports.c b/tests/test-libglnx-backports.c index 3683791d..89b1b3f7 100644 --- a/tests/test-libglnx-backports.c +++ b/tests/test-libglnx-backports.c @@ -1,9 +1,10 @@ /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * Copyright (C) 2011 Red Hat, Inc. * Copyright (C) 2018 Endless OS Foundation, LLC * Copyright 2019 Emmanuel Fleury - * Copyright 2021 Collabora Ltd. + * Copyright 2021-2024 Collabora Ltd. * SPDX-License-Identifier: LGPL-2.1-or-later AND LicenseRef-old-glib-tests */ @@ -11,6 +12,193 @@ #include "libglnx.h" #include +#include + +#include +#include + +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_subprocess_einval (void); + +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); + + 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++) + g_assert_no_errno (close (fds[i])); + + g_free (fds); + + if (g_test_undefined ()) + { +#if GLIB_CHECK_VERSION (2, 38, 0) + g_test_trap_subprocess ("/glib-unix/closefrom/subprocess/einval", + 0, G_TEST_SUBPROCESS_DEFAULT); +#else + if (g_test_trap_fork (0, 0)) + { + test_closefrom_subprocess_einval (); + exit (0); + } + +#endif + 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); +} /* Testing g_memdup2() function with various positive and negative cases */ static void @@ -98,6 +286,12 @@ test_strv_equal (void) int main (int argc, char **argv) { g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/glib-unix/closefrom", test_closefrom); +#if GLIB_CHECK_VERSION (2, 38, 0) + g_test_add_func ("/glib-unix/closefrom/subprocess/einval", + test_closefrom_subprocess_einval); +#endif g_test_add_func ("/mainloop/steal-fd", test_steal_fd); g_test_add_func ("/strfuncs/memdup2", test_memdup2); g_test_add_func ("/strfuncs/strv-equal", test_strv_equal);