Commit 83af0f45 for guacamole.apache.org
commit 83af0f45c3b2099297e85734d1eb4901f6d3e050
Author: Heyzi <aaplatonov@sberdevices.ru>
Date: Wed Aug 5 13:12:02 2026 +0300
GUACAMOLE-1625: Prefer close_range() when closing inherited descriptors.
Testing each descriptor individually costs a syscall per descriptor, which
close_range() avoids entirely. Where it is unavailable, the cost is bounded by
closing only those descriptors which are actually open, as the limit on the
number of descriptors may be orders of magnitude greater than the number in
use.
A C library may lack a wrapper for close_range() while the kernel still
provides the system call, in which case it is invoked directly. That is the
case for musl, and thus for the guacd Docker image.
Descriptors are no longer tested to determine whether they refer to sockets.
Every descriptor inherited here is a socket, and neither libuuid nor OpenSSL
retains one across the fork, both obtaining randomness through getrandom().
diff --git a/configure.ac b/configure.ac
index 61851b31..d0ffa2b3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -150,7 +150,7 @@ AC_SUBST(UUID_LIBS)
AC_SUBST(CUNIT_LIBS)
# Library functions
-AC_CHECK_FUNCS([clock_gettime gettimeofday memmove memset select strdup nanosleep prctl])
+AC_CHECK_FUNCS([clock_gettime gettimeofday memmove memset select strdup nanosleep prctl close_range])
AC_CHECK_DECL([png_get_io_ptr],
[AC_DEFINE([HAVE_PNG_GET_IO_PTR],,
diff --git a/src/guacd/proc.c b/src/guacd/proc.c
index d8fb0ad8..15a1dd54 100644
--- a/src/guacd/proc.c
+++ b/src/guacd/proc.c
@@ -40,7 +40,7 @@
#include <string.h>
#include <syslog.h>
#include <unistd.h>
-#include <sys/stat.h>
+#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
@@ -441,35 +441,51 @@ cleanup_process:
}
/**
- * Returns whether the given file descriptor refers to a socket.
+ * Closes all file descriptors within the given inclusive range, if the system
+ * provides a means of closing descriptors in bulk. That means is close_range(),
+ * which requires Linux 5.9 or FreeBSD, with glibc 2.34 providing the wrapper.
+ * See: https://man7.org/linux/man-pages/man2/close_range.2.html
*
- * @param fd
- * The file descriptor to test.
+ * @param first
+ * The first file descriptor in the range to be closed.
+ *
+ * @param last
+ * The last file descriptor in the range to be closed.
*
* @return
- * Non-zero if the given file descriptor refers to a socket, zero
- * otherwise.
+ * Zero if all descriptors within the given range were closed, non-zero if
+ * the range could not be closed, including if the system provides no means
+ * of closing descriptors in bulk.
*/
-static int guacd_is_socket(int fd) {
-
- struct stat fd_stat;
- return !fstat(fd, &fd_stat) && S_ISSOCK(fd_stat.st_mode);
+static int guacd_close_fd_range(unsigned int first, unsigned int last) {
+
+#if defined(HAVE_CLOSE_RANGE)
+ return close_range(first, last, 0);
+#elif defined(SYS_close_range)
+ /* musl has no wrapper, its maintainers having observed that callers wanting
+ * close_range() will invoke the system call directly. This applies to the
+ * guacd Docker image, which is based on Alpine.
+ * See: https://www.openwall.com/lists/musl/2022/08/18/5 */
+ return syscall(SYS_close_range, first, last, 0);
+#else
+ return -1;
+#endif
}
/**
- * Closes all sockets inherited from the parent guacd process, leaving only the
- * standard streams and the given descriptor open. This function may be called
- * only from within a newly-forked connection process.
+ * Closes all file descriptors inherited from the parent guacd process, leaving
+ * only the standard streams and the given descriptor open. This function may
+ * be called only from within a newly-forked connection process.
*
- * The sockets closed here belong to unrelated connections: the parent's end of
- * every other connection's user socketpair, the parent's end of every other
- * connection process' socketpair, and the socket guacd listens on. Retaining
- * them keeps those sockets referenced after the parent has closed them, such
- * that their peers never observe EOF, writes to those peers block indefinitely
- * rather than failing, and the processes owning them can never determine that
- * their users have left, nor exit. Note that FD_CLOEXEC cannot serve this
- * purpose, as connection processes are forked but never exec'd.
+ * The descriptors closed here belong to unrelated connections: the parent's
+ * end of every other connection's user socketpair, the parent's end of every
+ * other connection process' socketpair, and the socket guacd listens on.
+ * Retaining them keeps those sockets referenced after the parent has closed
+ * them, such that their peers never observe EOF, writes to those peers block
+ * indefinitely rather than failing, and the processes owning them can never
+ * determine that their users have left, nor exit. Note that FD_CLOEXEC cannot
+ * serve this purpose, as connection processes are forked but never exec'd.
*
* @param keep_fd
* The file descriptor which must be left open. This must be greater than
@@ -481,13 +497,24 @@ static void guacd_close_inherited_fds(int keep_fd) {
* continuing to write to whatever ends up occupying the one closed here */
closelog();
- /* Only sockets are closed. Descriptors for anything else may be held by
- * libraries unaware that they are being closed, such that reuse of the
- * same descriptor number would silently redirect their reads and writes. */
+ /* Everything except the standard streams and the descriptor being kept may
+ * be closed as at most two ranges */
+ if (!guacd_close_fd_range(keep_fd + 1, ~0U)) {
+
+ if (keep_fd <= STDERR_FILENO + 1)
+ return;
+ if (!guacd_close_fd_range(STDERR_FILENO + 1, keep_fd - 1))
+ return;
+
+ }
+
+ /* Distributions still under support may predate close_range() entirely,
+ * RHEL 8 pairing glibc 2.28 with Linux 4.18. Close only those descriptors
+ * which are actually open there, as the limit on the number of descriptors
+ * may be orders of magnitude greater than the number in use, and defaults
+ * to 1048576 within a container. */
#ifdef __linux__
- /* Enumerate only the descriptors actually open, as the limit on the number
- * of descriptors may be arbitrarily high */
DIR* open_fds = opendir("/proc/self/fd");
if (open_fds != NULL) {
@@ -499,10 +526,7 @@ static void guacd_close_inherited_fds(int keep_fd) {
/* Non-numeric entries ("." and "..") parse as zero and are thus
* skipped along with the standard streams */
int fd = atoi(entry->d_name);
- if (fd <= STDERR_FILENO || fd == keep_fd || fd == dir_fd)
- continue;
-
- if (guacd_is_socket(fd))
+ if (fd > STDERR_FILENO && fd != keep_fd && fd != dir_fd)
close(fd);
}
@@ -513,14 +537,14 @@ static void guacd_close_inherited_fds(int keep_fd) {
}
#endif
- /* Fall back to testing each descriptor which could possibly be open,
+ /* Fall back to closing each descriptor which could possibly be open,
* bounding the search arbitrarily if no limit can be determined */
long max_fd = sysconf(_SC_OPEN_MAX);
if (max_fd < 0)
max_fd = 1024;
for (long fd = STDERR_FILENO + 1; fd < max_fd; fd++) {
- if (fd != keep_fd && guacd_is_socket(fd))
+ if (fd != keep_fd)
close(fd);
}