Merge branch 'wip/smcv/closefrom' into 'main'

glib-unix: Add g_closefrom(), g_fdwalk_set_cloexec()

Closes #3247

See merge request GNOME/glib!3884
This commit is contained in:
Philip Withnall 2024-02-09 14:06:52 +00:00
commit 8300b52cbb
4 changed files with 619 additions and 346 deletions

View File

@ -1,6 +1,20 @@
/* GLIB - Library of useful routines for C programming /* GLIB - Library of useful routines for C programming
* Copyright (C) 2011 Red Hat, Inc. * Copyright 2000-2022 Red Hat, Inc.
* Copyright 2023 Collabora Ltd. * 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 * glib-unix.c: UNIX specific API wrappers and convenience functions
* *
@ -28,9 +42,27 @@
#include "glib-unixprivate.h" #include "glib-unixprivate.h"
#include "gmain-internal.h" #include "gmain-internal.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h> /* for fdwalk */
#include <string.h> #include <string.h>
#include <sys/types.h> #include <sys/types.h>
#include <pwd.h> #include <pwd.h>
#include <unistd.h>
#if defined(__linux__) || defined(__DragonFly__)
#include <sys/syscall.h> /* for syscall and SYS_getdents64 */
#endif
#ifdef HAVE_SYS_RESOURCE_H
#include <sys/resource.h>
#endif /* HAVE_SYS_RESOURCE_H */
#if defined(__APPLE__) && defined(HAVE_LIBPROC_H)
#include <libproc.h>
#include <sys/proc_info.h>
#endif
G_STATIC_ASSERT (sizeof (ssize_t) == GLIB_SIZEOF_SSIZE_T); G_STATIC_ASSERT (sizeof (ssize_t) == GLIB_SIZEOF_SSIZE_T);
G_STATIC_ASSERT (G_ALIGNOF (gssize) == G_ALIGNOF (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); 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 its 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 its 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
}

View File

@ -326,6 +326,12 @@ g_unix_pipe_clear (GUnixPipe *self)
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (GUnixPipe, g_unix_pipe_clear) 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_GNUC_END_IGNORE_DEPRECATIONS
G_END_DECLS G_END_DECLS

View File

@ -1194,13 +1194,10 @@ write_err_and_exit (gint fd, gint msg)
/* This function is called between fork() and exec() and hence must be /* This function is called between fork() and exec() and hence must be
* async-signal-safe (see signal-safety(7)). */ * async-signal-safe (see signal-safety(7)). */
static int static void
set_cloexec (void *data, gint fd) set_cloexec (int fd)
{ {
if (fd >= GPOINTER_TO_INT (data))
fcntl (fd, F_SETFD, FD_CLOEXEC); fcntl (fd, F_SETFD, FD_CLOEXEC);
return 0;
} }
/* This function is called between fork() and exec() and hence must be /* This function is called between fork() and exec() and hence must be
@ -1265,337 +1262,6 @@ dupfd_cloexec (int old_fd, int new_fd_min)
return fd; 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 its 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 its 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 /* This function is called between fork() and exec() and hence must be
* async-signal-safe (see signal-safety(7)). */ * async-signal-safe (see signal-safety(7)). */
static gint static gint
@ -1727,7 +1393,7 @@ do_exec (gint child_err_report_fd,
write_err_and_exit (child_err_report_fd, write_err_and_exit (child_err_report_fd,
CHILD_DUPFD_FAILED); CHILD_DUPFD_FAILED);
set_cloexec (GINT_TO_POINTER(0), stdin_fd); set_cloexec (stdin_fd);
} }
else if (!child_inherits_stdin) else if (!child_inherits_stdin)
{ {
@ -1765,7 +1431,7 @@ do_exec (gint child_err_report_fd,
write_err_and_exit (child_err_report_fd, write_err_and_exit (child_err_report_fd,
CHILD_DUPFD_FAILED); CHILD_DUPFD_FAILED);
set_cloexec (GINT_TO_POINTER(0), stdout_fd); set_cloexec (stdout_fd);
} }
else if (stdout_to_null) else if (stdout_to_null)
{ {
@ -1797,7 +1463,7 @@ do_exec (gint child_err_report_fd,
write_err_and_exit (child_err_report_fd, write_err_and_exit (child_err_report_fd,
CHILD_DUPFD_FAILED); CHILD_DUPFD_FAILED);
set_cloexec (GINT_TO_POINTER(0), stderr_fd); set_cloexec (stderr_fd);
} }
else if (stderr_to_null) else if (stderr_to_null)
{ {
@ -1822,21 +1488,21 @@ do_exec (gint child_err_report_fd,
{ {
if (safe_dup2 (child_err_report_fd, 3) < 0) if (safe_dup2 (child_err_report_fd, 3) < 0)
write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED); write_err_and_exit (child_err_report_fd, CHILD_DUPFD_FAILED);
set_cloexec (GINT_TO_POINTER (0), 3); set_cloexec (3);
if (safe_closefrom (4) < 0) if (g_closefrom (4) < 0)
write_err_and_exit (child_err_report_fd, CHILD_CLOSE_FAILED); write_err_and_exit (child_err_report_fd, CHILD_CLOSE_FAILED);
child_err_report_fd = 3; child_err_report_fd = 3;
} }
else 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); write_err_and_exit (child_err_report_fd, CHILD_CLOSE_FAILED);
} }
} }
else else
{ {
/* We need to do child_err_report_fd anyway */ /* 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);
} }
/* /*

View File

@ -36,6 +36,187 @@
#include "testutils.h" #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 static void
test_pipe (void) test_pipe (void)
{ {
@ -613,6 +794,9 @@ main (int argc,
{ {
g_test_init (&argc, &argv, NULL); 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", test_pipe);
g_test_add_func ("/glib-unix/pipe/fd-cloexec", test_pipe_fd_cloexec); 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); g_test_add_func ("/glib-unix/pipe-stdio-overwrite", test_pipe_stdio_overwrite);