Commit c6884bcc for guacamole.apache.org
commit c6884bcc269faf0a7af1aeef933fc57798db3e0a
Author: Michael Jumper <mjumper@apache.org>
Date: Fri Sep 25 18:08:51 2026 -0700
GUACAMOLE-2118: Migrate to re-exec after fork(), harden against unexpected behavior.
diff --git a/configure.ac b/configure.ac
index d0ffa2b3..bea7c17f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -41,7 +41,20 @@ AC_PROG_LIBTOOL
PKG_PROG_PKG_CONFIG()
# Headers
-AC_CHECK_HEADERS([fcntl.h stdlib.h string.h sys/socket.h time.h sys/time.h syslog.h unistd.h cairo/cairo.h pngstruct.h])
+AC_CHECK_HEADERS(m4_normalize([
+ cairo/cairo.h
+ fcntl.h
+ pngstruct.h
+ stdlib.h
+ string.h
+ syslog.h
+ sys/prctl.h
+ sys/procctl.h
+ sys/socket.h
+ sys/time.h
+ time.h
+ unistd.h
+]))
# Source characteristics
AC_DEFINE([_GNU_SOURCE], [1], [Uses GNU-specific APIs (if available)])
@@ -150,7 +163,19 @@ AC_SUBST(UUID_LIBS)
AC_SUBST(CUNIT_LIBS)
# Library functions
-AC_CHECK_FUNCS([clock_gettime gettimeofday memmove memset select strdup nanosleep prctl close_range])
+AC_CHECK_FUNCS(m4_normalize([
+ accept4
+ clock_gettime
+ close_range
+ gettimeofday
+ memmove
+ memset
+ nanosleep
+ pipe2
+ prctl
+ select
+ strdup
+]))
AC_CHECK_DECL([png_get_io_ptr],
[AC_DEFINE([HAVE_PNG_GET_IO_PTR],,
diff --git a/src/guacd/Makefile.am b/src/guacd/Makefile.am
index 72d6d9ec..f5baf374 100644
--- a/src/guacd/Makefile.am
+++ b/src/guacd/Makefile.am
@@ -42,6 +42,7 @@ noinst_HEADERS = \
log.h \
move-fd.h \
proc.h \
+ proc-child.h \
proc-map.h
guacd_SOURCES = \
@@ -53,6 +54,7 @@ guacd_SOURCES = \
log.c \
move-fd.c \
proc.c \
+ proc-child.c \
proc-map.c
guacd_CFLAGS = \
diff --git a/src/guacd/conf-file.c b/src/guacd/conf-file.c
index aded4fad..2177da7a 100644
--- a/src/guacd/conf-file.c
+++ b/src/guacd/conf-file.c
@@ -194,7 +194,7 @@ guacd_config* guacd_conf_load() {
#endif
/* Read configuration from file */
- int fd = open(GUACD_CONF_FILE, O_RDONLY);
+ int fd = open(GUACD_CONF_FILE, O_RDONLY | O_CLOEXEC);
/* Notify of errors preventing reading */
if (fd < 0) {
diff --git a/src/guacd/connection.c b/src/guacd/connection.c
index cd5ca64e..a5e4070d 100644
--- a/src/guacd/connection.c
+++ b/src/guacd/connection.c
@@ -23,6 +23,7 @@
#include "proc.h"
#include "proc-map.h"
+#include <guacamole/assert.h>
#include <guacamole/client.h>
#include <guacamole/error.h>
#include <guacamole/mem.h>
@@ -43,7 +44,6 @@
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
-#include <sys/wait.h>
/**
* Behaves exactly as write(), but writes as much as possible, returning
@@ -133,7 +133,7 @@ static void* guacd_connection_write_thread(void* data) {
* which vanishes without sending "disconnect" leaves that process blocked
* awaiting input indefinitely, as nothing further will inform it that its
* last user has left. */
- if (shutdown(params->fd, SHUT_WR))
+ if (shutdown(params->fd, SHUT_WR) && errno != ENOTCONN)
guacd_log(GUAC_LOG_ERROR, "Unable to signal end of user input to "
"connection process: %s. That process may remain running but "
"inactive, retaining the memory of its connection until guacd "
@@ -155,22 +155,32 @@ void* guacd_connection_io_thread(void* data) {
int length;
pthread_t write_thread;
- pthread_create(&write_thread, NULL, guacd_connection_write_thread, params);
+ int result = pthread_create(&write_thread, NULL, guacd_connection_write_thread, params);
+ if (result) {
+ guacd_log(GUAC_LOG_ERROR, "Unable to start write thread for user: %s.", strerror(result));
+ guac_parser_free(params->parser);
+ }
- /* Transfer data from file descriptor to socket */
- while (1) {
- GUAC_RETRY_EINTR(length, read(params->fd, buffer, sizeof(buffer)));
+ else {
- if (length <= 0)
- break;
+ /* Transfer data from file descriptor to socket */
+ while (1) {
+ GUAC_RETRY_EINTR(length, read(params->fd, buffer, sizeof(buffer)));
- if (guac_socket_write(params->socket, buffer, length))
- break;
- guac_socket_flush(params->socket);
- }
+ if (length <= 0)
+ break;
+
+ if (guac_socket_write(params->socket, buffer, length))
+ break;
+ guac_socket_flush(params->socket);
+ }
- /* Wait for write thread to die */
- pthread_join(write_thread, NULL);
+ /* Stop blocking on any outstanding I/O and wait for write thread to
+ * die */
+ guac_socket_shutdown(params->socket);
+ pthread_join(write_thread, NULL);
+
+ }
/* Clean up */
guac_socket_free(params->socket);
@@ -182,16 +192,16 @@ void* guacd_connection_io_thread(void* data) {
}
/**
- * Adds the given socket as a new user to the given process, automatically
- * reading/writing from the socket via read/write threads. The given socket,
- * parser, and any associated resources will be freed unless the user is not
- * added successfully.
+ * Adds the given socket as a new user of the process reached by the given
+ * descriptor, automatically reading/writing from the socket via read/write
+ * threads. The given socket, parser, and any associated resources will be
+ * freed unless the user is not added successfully.
*
* If adding the user fails for any reason, non-zero is returned. Zero is
* returned upon success.
*
- * @param proc
- * The existing process to add the user to.
+ * @param proc_socket
+ * The socket of the process to add the user to.
*
* @param parser
* The parser associated with the given guac_socket (used to handle the
@@ -204,12 +214,12 @@ void* guacd_connection_io_thread(void* data) {
* @return
* Zero if the user was added successfully, non-zero if an error occurred.
*/
-static int guacd_add_user(guacd_proc* proc, guac_parser* parser, guac_socket* socket) {
+static int guacd_add_user(int proc_socket, guac_parser* parser, guac_socket* socket) {
int sockets[2];
/* Set up socket pair */
- if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0) {
+ if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets) < 0) {
guacd_log(GUAC_LOG_ERROR, "Unable to allocate file descriptors for I/O transfer: %s", strerror(errno));
return 1;
}
@@ -217,9 +227,21 @@ static int guacd_add_user(guacd_proc* proc, guac_parser* parser, guac_socket* so
int user_fd = sockets[0];
int proc_fd = sockets[1];
+ /* Bound how long any connection socket write may block (an indefinitely
+ * blocking write would hide the end of the connection) */
+ if (setsockopt(user_fd, SOL_SOCKET, SO_SNDTIMEO,
+ &GUACD_TIMEOUT_TIMEVAL, sizeof(GUACD_TIMEOUT_TIMEVAL))) {
+ guacd_log(GUAC_LOG_WARNING, "Unable to set write timeout on internal "
+ "connection socket: %s. A blocked connection process may "
+ "prevent this user's disconnection from being observed.",
+ strerror(errno));
+ }
+
/* Send user file descriptor to process */
- if (!guacd_send_fd(proc->fd_socket, proc_fd)) {
+ if (!guacd_send_fd(proc_socket, proc_fd)) {
guacd_log(GUAC_LOG_ERROR, "Unable to add user.");
+ close(proc_fd);
+ close(user_fd);
return 1;
}
@@ -227,13 +249,25 @@ static int guacd_add_user(guacd_proc* proc, guac_parser* parser, guac_socket* so
close(proc_fd);
guacd_connection_io_thread_params* params = guac_mem_alloc(sizeof(guacd_connection_io_thread_params));
+ if (params == NULL) {
+ guacd_log(GUAC_LOG_ERROR, "Unable to allocate I/O transfer state for user.");
+ close(user_fd);
+ return 1;
+ }
+
params->parser = parser;
params->socket = socket;
params->fd = user_fd;
/* Start I/O thread */
pthread_t io_thread;
- pthread_create(&io_thread, NULL, guacd_connection_io_thread, params);
+ int result = pthread_create(&io_thread, NULL, guacd_connection_io_thread, params);
+ if (result) {
+ guacd_log(GUAC_LOG_ERROR, "Unable to start I/O thread for user: %s.", strerror(result));
+ close(user_fd);
+ guac_mem_free(params);
+ return 1;
+ }
pthread_detach(io_thread);
return 0;
@@ -256,11 +290,16 @@ static int guacd_add_user(guacd_proc* proc, guac_parser* parser, guac_socket* so
* The socket associated with the new connection that must be routed to
* a new or existing process within the given map.
*
+ * @param exe_fd
+ * A descriptor pointing to the guacd executable that should be executed to
+ * handle the new connection.
+ *
* @return
* Zero if the connection was successfully routed, non-zero if routing has
* failed.
*/
-static int guacd_route_connection(guacd_proc_map* map, guac_socket* socket) {
+static int guacd_route_connection(guacd_proc_map* map, guac_socket* socket,
+ int exe_fd) {
guac_parser* parser = guac_parser_alloc();
@@ -292,22 +331,30 @@ static int guacd_route_connection(guacd_proc_map* map, guac_socket* socket) {
return 1;
}
- guacd_proc* proc;
+ guacd_proc* proc = NULL;
+ int proc_socket;
int new_process;
const char* identifier = parser->argv[0];
- /* If connection ID, retrieve existing process */
+ /* If connection ID, join existing process */
if (identifier[0] == GUAC_CLIENT_ID_PREFIX) {
- proc = guacd_proc_map_retrieve(map, identifier);
+ proc_socket = guacd_proc_map_open(map, identifier);
new_process = 0;
/* Warn and ward off client if requested connection does not exist */
- if (proc == NULL) {
- guacd_log(GUAC_LOG_INFO, "Connection \"%s\" does not exist", identifier);
- guac_protocol_send_error(socket, "No such connection.",
- GUAC_PROTOCOL_STATUS_RESOURCE_NOT_FOUND);
+ if (proc_socket < 0) {
+ if (guac_error == GUAC_STATUS_NOT_FOUND) {
+ guacd_log(GUAC_LOG_INFO, "Connection \"%s\" does not exist", identifier);
+ guac_protocol_send_error(socket, "No such connection.",
+ GUAC_PROTOCOL_STATUS_RESOURCE_NOT_FOUND);
+ }
+ else {
+ guacd_log_guac_error(GUAC_LOG_ERROR, "Unable to join connection");
+ guac_protocol_send_error(socket, "Unable to join connection.",
+ GUAC_PROTOCOL_STATUS_SERVER_ERROR);
+ }
}
else
@@ -323,20 +370,21 @@ static int guacd_route_connection(guacd_proc_map* map, guac_socket* socket) {
identifier);
/* Create new process */
- proc = guacd_create_proc(identifier);
+ proc = guacd_create_proc(identifier, exe_fd);
+ proc_socket = (proc != NULL) ? proc->fd_socket : -1;
new_process = 1;
}
/* Abort if no process exists for the requested connection */
- if (proc == NULL) {
+ if (proc_socket < 0) {
guacd_log_guac_error(GUAC_LOG_INFO, "Connection did not succeed");
guac_parser_free(parser);
return 1;
}
/* Add new user (in the case of a new process, this will be the owner */
- int add_user_failed = guacd_add_user(proc, parser, socket);
+ int add_user_failed = guacd_add_user(proc_socket, parser, socket);
/* If new process was created, manage that process */
if (new_process) {
@@ -349,36 +397,49 @@ static int guacd_route_connection(guacd_proc_map* map, guac_socket* socket) {
proc->client->connection_id);
/* Store process, allowing other users to join */
- guacd_proc_map_add(map, proc);
-
- /* Wait for child to finish */
- pid_t wait_result;
- GUAC_RETRY_EINTR(wait_result, waitpid(proc->pid, NULL, 0));
-
- /* Remove client */
- if (guacd_proc_map_remove(map, proc->client->connection_id) == NULL)
- guacd_log(GUAC_LOG_ERROR, "Internal failure removing "
- "client \"%s\". Client record will never be freed.",
- proc->client->connection_id);
- else
- guacd_log(GUAC_LOG_INFO, "Connection \"%s\" removed.",
- proc->client->connection_id);
+ int duplicate = guacd_proc_map_add(map, proc);
+ GUAC_ASSERT(!duplicate);
}
- /* Parser must be manually freed if the process did not start */
- else
+ /* Stop the process and free the parser manually if the owner could
+ * not be added */
+ else {
guac_parser_free(parser);
+ guacd_proc_stop(proc);
+ }
+
+ /* Wait for child to finish */
+ guacd_proc_await_exit(proc);
+
+ /* Stop routing users before the group is swept */
+ if (!add_user_failed) {
+ int missing = guacd_proc_map_stop_routing(map, proc->client->connection_id);
+ GUAC_ASSERT(!missing);
+ }
- /* Force process to stop and clean up */
guacd_proc_stop(proc);
- /* Free skeleton client */
- guac_client_free(proc->client);
+ if (!add_user_failed) {
+ int missing = guacd_proc_map_remove(map, proc->client->connection_id);
+ GUAC_ASSERT(!missing);
+ guacd_log(GUAC_LOG_INFO, "Connection \"%s\" removed.",
+ proc->client->connection_id);
+ }
/* Clean up */
- close(proc->fd_socket);
- guac_mem_free(proc);
+ guacd_proc_free(proc);
+
+ }
+
+ else {
+
+ close(proc_socket);
+
+ /* The parser must also be manually freed if the user could not be
+ * added to an existing process */
+ if (add_user_failed)
+ guac_parser_free(parser);
}
@@ -396,6 +457,7 @@ void* guacd_connection_thread(void* data) {
guacd_connection_thread_params* params = (guacd_connection_thread_params*) data;
guacd_proc_map* map = params->map;
+ int exe_fd = params->exe_fd;
int connected_socket_fd = params->connected_socket_fd;
guac_socket* socket;
@@ -422,8 +484,15 @@ void* guacd_connection_thread(void* data) {
socket = guac_socket_open(connected_socket_fd);
#endif
+ if (socket == NULL) {
+ guacd_log_guac_error(GUAC_LOG_ERROR, "Unable to open socket for connection");
+ close(connected_socket_fd);
+ guac_mem_free(params);
+ return NULL;
+ }
+
/* Route connection according to Guacamole, creating a new process if needed */
- if (guacd_route_connection(map, socket))
+ if (guacd_route_connection(map, socket, exe_fd))
guac_socket_free(socket);
guac_mem_free(params);
diff --git a/src/guacd/connection.h b/src/guacd/connection.h
index 53e9b4ab..64e1a2de 100644
--- a/src/guacd/connection.h
+++ b/src/guacd/connection.h
@@ -36,6 +36,12 @@ typedef struct guacd_connection_thread_params {
*/
guacd_proc_map* map;
+ /**
+ * A file descriptor pointing to the executable of the running copy of
+ * guacd. This value is used by guacd_create_proc() to execute subprocesses.
+ */
+ int exe_fd;
+
#ifdef ENABLE_SSL
/**
* SSL context for encrypted connections to guacd. If SSL is not active,
@@ -61,11 +67,11 @@ typedef struct guacd_connection_thread_params {
*
* @param data
* A pointer to a guacd_connection_thread_params structure containing the
- * shared overall map of currently-connected processes, the file
- * descriptor associated with the newly-established connection that is to
- * be either (1) associated with a new process or (2) passed on to an
- * existing process, and the SSL context for the encryption surrounding
- * that connection (if any).
+ * shared overall map of currently-connected processes, the file descriptor
+ * pointing to the guacd executable, the file descriptor associated with the
+ * newly-established connection that is to be either (1) associated with a
+ * new process or (2) passed on to an existing process, and the SSL context
+ * for the encryption surrounding that connection (if any).
*
* @return
* Always NULL.
diff --git a/src/guacd/daemon.c b/src/guacd/daemon.c
index ec5f531b..d9602c26 100644
--- a/src/guacd/daemon.c
+++ b/src/guacd/daemon.c
@@ -22,10 +22,13 @@
#include "conf-file.h"
#include "connection.h"
#include "log.h"
+#include "proc-child.h"
#include "proc-map.h"
+#include <guacamole/assert.h>
#include <guacamole/mem.h>
#include <guacamole/proctitle.h>
+#include <guacamole/timestamp.h>
#ifdef ENABLE_SSL
#include <openssl/ssl.h>
@@ -42,9 +45,6 @@
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
-#ifdef HAVE_PRCTL
-#include <sys/prctl.h>
-#endif
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
@@ -54,6 +54,12 @@
#define GUACD_DEV_NULL "/dev/null"
#define GUACD_ROOT "/"
+/**
+ * The number of milliseconds to wait between checks for remaining connection
+ * processes while waiting for all connections to terminate during shutdown.
+ */
+#define GUACD_CLEANUP_POLL_INTERVAL 100
+
/**
* Redirects the given file descriptor to /dev/null. The given flags must match
* the read/write flags of the file descriptor given (if the given file
@@ -86,6 +92,37 @@ static int redirect_fd(int fd, int flags) {
}
+/**
+ * Ensures the given file descriptor is open. If not open, the file descriptor
+ * is opened with the given flags and redirected to /dev/null. If open already,
+ * the given flags are ignored.
+ *
+ * @param fd
+ * The file descriptor that should be open.
+ *
+ * @param flags
+ * The read/write flags that should be applied if the descriptor must be
+ * opened.
+ *
+ * @return
+ * Zero if the descriptor is open or was successfully redirected,
+ * non-zero otherwise.
+ */
+static int ensure_open(int fd, int flags) {
+
+ /* Check whether descriptor is already open */
+ if (fcntl(fd, F_GETFD) >= 0)
+ return 0;
+
+ /* Bail out if an unexpected failure prevented checking whether the
+ * descriptor was open */
+ if (errno != EBADF)
+ return 1;
+
+ return redirect_fd(fd, flags);
+
+}
+
/**
* Turns the current process into a daemon through a series of fork() calls.
* The standard I/O file descriptors for STDIN, STDOUT, and STDERR will be
@@ -293,10 +330,73 @@ static void stop_process_callback(guacd_proc* proc, void* data) {
}
+/**
+ * A callback for guacd_proc_map_foreach which sends SIGKILL to the process and
+ * process group of every guacd_proc in the map.
+ *
+ * @param proc
+ * The guacd process whose group should be killed.
+ *
+ * @param data
+ * A pointer to the int that should be set to non-zero if any process was
+ * still running and sent SIGKILL.
+ */
+static void kill_process_callback(guacd_proc* proc, void* data) {
+
+ int* killed = (int*) data;
+ int still_running = guacd_proc_is_running(proc);
+
+ /* The process group is killed even when the original process is no longer
+ * running, as orphaned child processes are not otherwise observable */
+ kill(-proc->pid, SIGKILL);
+
+ if (still_running) {
+ kill(proc->pid, SIGKILL);
+ *killed = 1;
+ }
+
+}
+
+/**
+ * A callback for guacd_proc_map_foreach which counts the processes remaining
+ * within the map.
+ *
+ * @param proc
+ * The guacd process being counted.
+ *
+ * @param data
+ * A pointer to the int that should receive the count. The integer must be
+ * initialized to zero.
+ */
+static void count_process_callback(guacd_proc* proc, void* data) {
+ (*(int*) data)++;
+}
+
int main(int argc, char* argv[]) {
guac_process_title_init(argc, argv);
+ /* Run as per-connection child process if requested by guacd parent (as
+ * indicated by process name) */
+ if (strncmp(argv[0], GUACD_PROC_NAME_PREFIX, strlen(GUACD_PROC_NAME_PREFIX)) == 0) {
+ guacd_connection_process(argc, argv);
+ GUAC_ASSERT(0); /* guacd_connection_process() does not return */
+ }
+
+ /* Ensure STDIN/STDOUT/STDERR are open - if any is closed, future output may
+ * go to whatever is opened next. The descriptors a connection process
+ * inherits are reserved the same way, such that nothing opened by this
+ * process can occupy them (see guacd_create_proc()). */
+ if (ensure_open(STDIN_FILENO, O_RDONLY)
+ || ensure_open(STDOUT_FILENO, O_WRONLY)
+ || ensure_open(STDERR_FILENO, O_WRONLY)
+ || ensure_open(GUACD_PROC_SOCKET_FD, O_RDWR)
+ || ensure_open(GUACD_PROC_CONFIG_FD, O_RDWR)) {
+ guacd_log(GUAC_LOG_ERROR, "Unable to ensure standard and reserved file "
+ "descriptors are open. Bailing out.");
+ exit(EXIT_FAILURE);
+ }
+
/* Server */
int socket_fd;
struct addrinfo* addresses;
@@ -344,6 +444,15 @@ int main(int argc, char* argv[]) {
/* Log start */
guacd_log(GUAC_LOG_INFO, "Guacamole proxy daemon (guacd) version " VERSION " started");
+ int exe_fd = guacd_open_exe(argv[0]);
+ if (exe_fd < 0) {
+ guacd_log(GUAC_LOG_ERROR, "Bailing out, as the executable for the "
+ "running guacd instance could not be opened (future "
+ "connections will not be able to start): %s",
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
/* Get addresses for binding */
if ((retval = getaddrinfo(config->bind_host, config->bind_port,
&hints, &addresses))) {
@@ -370,7 +479,7 @@ int main(int argc, char* argv[]) {
gai_strerror(retval));
/* Get socket */
- socket_fd = socket(current_address->ai_family, SOCK_STREAM, 0);
+ socket_fd = socket(current_address->ai_family, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (socket_fd < 0) {
guacd_log(GUAC_LOG_ERROR, "Error opening socket: %s", strerror(errno));
@@ -501,11 +610,8 @@ int main(int argc, char* argv[]) {
"SIGPIPE may cause termination of the daemon.");
}
- /* Ignore SIGCHLD (force automatic removal of children) */
- if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) {
- guacd_log(GUAC_LOG_INFO, "Could not set handler for SIGCHLD to ignore. "
- "Child processes may pile up in the process table.");
- }
+ /* NOTE: SIGCHLD is left with its default handling, as each child PID must be
+ * reliable until guacd_proc_free() reaps it */
/* Clean up and exit if SIGINT or SIGTERM signals are caught; don't set
SA_RESTART as we rely on accept() to return EINTR.*/
@@ -513,14 +619,6 @@ int main(int argc, char* argv[]) {
sigaction(SIGINT, &signal_stop_action, NULL);
sigaction(SIGTERM, &signal_stop_action, NULL);
-#ifdef HAVE_PRCTL
- /* Adopt any orphan processes from clients so we can properly terminate and reap */
- prctl(PR_SET_CHILD_SUBREAPER, 1);
-#else
- guacd_log(GUAC_LOG_INFO, "prctl not available on this platform. "
- "Orphan child processes may pile up in the process table.");
-#endif
-
/* Log listening status */
guacd_log(GUAC_LOG_INFO, "Listening on host %s, port %s", bound_address, bound_port);
@@ -540,8 +638,16 @@ int main(int argc, char* argv[]) {
/* Accept connection */
client_addr_len = sizeof(client_addr);
+#ifdef HAVE_ACCEPT4
+ connected_socket_fd = accept4(socket_fd,
+ (struct sockaddr*) &client_addr, &client_addr_len,
+ SOCK_CLOEXEC);
+#else
connected_socket_fd = accept(socket_fd,
(struct sockaddr*) &client_addr, &client_addr_len);
+ if (connected_socket_fd >= 0)
+ fcntl(connected_socket_fd, F_SETFD, FD_CLOEXEC);
+#endif
if (connected_socket_fd < 0) {
if (errno == EINTR)
@@ -558,22 +664,42 @@ int main(int argc, char* argv[]) {
(const void*) &SO_TRUE, sizeof(SO_TRUE)))
guacd_log(GUAC_LOG_WARNING, "Unable to set TCP_NODELAY on socket: %s", strerror(errno));
+ /* Bound how long a read from or write to the connected user may block
+ * (a full kernel buffer may otherwise block cleanup) */
+ if (setsockopt(connected_socket_fd, SOL_SOCKET, SO_SNDTIMEO,
+ &GUACD_TIMEOUT_TIMEVAL, sizeof(GUACD_TIMEOUT_TIMEVAL)))
+ guacd_log(GUAC_LOG_WARNING, "Unable to set write timeout on socket: %s", strerror(errno));
+
+ if (setsockopt(connected_socket_fd, SOL_SOCKET, SO_RCVTIMEO,
+ &GUACD_TIMEOUT_TIMEVAL, sizeof(GUACD_TIMEOUT_TIMEVAL)))
+ guacd_log(GUAC_LOG_WARNING, "Unable to set read timeout on socket: %s", strerror(errno));
+
/* Create parameters for connection thread */
guacd_connection_thread_params* params = guac_mem_alloc(sizeof(guacd_connection_thread_params));
if (params == NULL) {
guacd_log(GUAC_LOG_ERROR, "Could not create connection thread: %s", strerror(errno));
+ close(connected_socket_fd);
continue;
}
params->map = map;
+ params->exe_fd = exe_fd;
params->connected_socket_fd = connected_socket_fd;
#ifdef ENABLE_SSL
params->ssl_context = ssl_context;
#endif
- /* Spawn thread to handle connection */
- pthread_create(&child_thread, NULL, guacd_connection_thread, params);
+ /* Spawn thread to handle connection, dropping the connection if that
+ * thread cannot be started */
+ int result = pthread_create(&child_thread, NULL,
+ guacd_connection_thread, params);
+ if (result) {
+ guacd_log(GUAC_LOG_ERROR, "Unable to start connection thread: %s.", strerror(result));
+ close(connected_socket_fd);
+ guac_mem_free(params);
+ continue;
+ }
pthread_detach(child_thread);
}
@@ -583,17 +709,34 @@ int main(int argc, char* argv[]) {
guacd_proc_map_foreach(map, stop_process_callback, NULL);
+ /* Wait up to GUACD_CLEANUP_TIMEOUT for all connections to end */
+ guac_timestamp cleanup_deadline = guac_timestamp_current() + GUACD_CLEANUP_TIMEOUT;
+ for (;;) {
+
+ int remaining = 0;
+ guacd_proc_map_foreach(map, count_process_callback, &remaining);
+ if (remaining == 0 || guac_timestamp_current() >= cleanup_deadline)
+ break;
+
+ guac_timestamp_msleep(GUACD_CLEANUP_POLL_INTERVAL);
+
+ }
+
+ /* Anything remaining has failed to stop in a timely manner and must be
+ * killed */
+ int killed = 0;
+ guacd_proc_map_foreach(map, kill_process_callback, &killed);
+
+ if (killed) {
+ guacd_log(GUAC_LOG_WARNING, "Not all connections terminated in a "
+ "timely manner. Any connections that failed to stop when "
+ "requested were forcibly terminated.");
+ }
+
/*
* FIXME: Clean up the proc map. This is not as straightforward as it
- * might seem, since the detached connection threads will attempt to
- * remove the connection processes from the map when they complete,
- * which will also happen upon shutdown. So there's a good chance that
- * this map cleanup will happen at the same time as the thread cleanup.
- * The map _does_ have locking mechanisms in place for ensuring thread
- * safety, but cleaning up the map also requires destroying those locks,
- * making them unusable for this case. One potential fix could be to
- * join every one of the connection threads instead of detaching them,
- * but that does complicate the cleanup of thread resources.
+ * might seem, since connection threads may still be in the handshake
+ * phase without any entries in the proc map.
*/
}
diff --git a/src/guacd/move-fd.c b/src/guacd/move-fd.c
index 5ed3307a..2680d34e 100644
--- a/src/guacd/move-fd.c
+++ b/src/guacd/move-fd.c
@@ -87,7 +87,7 @@ int guacd_recv_fd(int sock) {
/* Receive file descriptor */
ssize_t result;
- GUAC_RETRY_EINTR(result, recvmsg(sock, &message, 0));
+ GUAC_RETRY_EINTR(result, recvmsg(sock, &message, MSG_CMSG_CLOEXEC));
if (result == sizeof(message_data)) {
@@ -97,21 +97,43 @@ int guacd_recv_fd(int sock) {
return -1;
}
+ /* Truncated ancillary data means no room for another descriptor, or a
+ * policy blocked it */
+ int truncated = (message.msg_flags & MSG_CTRUNC) != 0;
+
/* Iterate control headers, looking for the sent file descriptor */
struct cmsghdr* control;
for (control = CMSG_FIRSTHDR(&message); control != NULL; control = CMSG_NXTHDR(&message, control)) {
- /* Pull file descriptor from data */
- if (control->cmsg_level == SOL_SOCKET && control->cmsg_type == SCM_RIGHTS) {
+ /* Pull file descriptor from data (if present) */
+ if (control->cmsg_level == SOL_SOCKET && control->cmsg_type == SCM_RIGHTS
+ && control->cmsg_len == CMSG_LEN(sizeof(fd))) {
+
memcpy(&fd, CMSG_DATA(control), sizeof(fd));
+
+ /* We can't use any descriptors included in a truncated
+ * SCM_RIGHTS message, but they should at least be closed */
+ if (truncated) {
+ close(fd);
+ break;
+ }
+
return fd;
+
}
}
+ /* The message arrived, but did not include a usable descriptor */
+ errno = truncated ? EMFILE : EPROTO;
+ return -1;
+
} /* end if recvmsg() success */
/* Failed to receive file descriptor */
+ if (result >= 0)
+ errno = ECONNRESET;
+
return -1;
}
diff --git a/src/guacd/proc-child.c b/src/guacd/proc-child.c
new file mode 100644
index 00000000..d22da8ab
--- /dev/null
+++ b/src/guacd/proc-child.c
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "log.h"
+#include "proc.h"
+#include "proc-child.h"
+
+#include <guacamole/assert.h>
+#include <guacamole/client.h>
+#include <guacamole/error.h>
+#include <guacamole/mem.h>
+#include <guacamole/proctitle.h>
+#include <guacamole/string.h>
+#include <guacamole/timestamp.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#ifdef HAVE_SYS_PRCTL_H
+#include <sys/prctl.h>
+#endif
+
+#ifdef HAVE_SYS_PROCCTL_H
+#include <sys/procctl.h>
+#endif
+
+/**
+ * Whether this platform can deliver a signal to a child process upon the death
+ * of its parent. Where unsupported, guacd_parent_watch_thread() polls the
+ * status of the parent process instead.
+ */
+#if defined(PR_SET_PDEATHSIG) || defined(PROC_PDEATHSIG_CTL)
+#define GUACD_HAVE_PDEATHSIG 1
+#else
+#define GUACD_HAVE_PDEATHSIG 0
+#endif
+
+#if !GUACD_HAVE_PDEATHSIG
+/**
+ * Thread which polls for the death of the parent guacd process, requesting
+ * termination of the current (child) process once the parent has died.
+ *
+ * @param data
+ * An intptr_t containing the PID of the parent guacd process.
+ *
+ * @return
+ * Always NULL.
+ */
+static void* guacd_parent_watch_thread(void* data) {
+
+ guac_thread_name_set("parent-watch");
+
+ /* Parent process has died if the current process has been re-parented (the
+ * parent PID has changed) */
+ pid_t parent_pid = (pid_t) (intptr_t) data;
+ while (getppid() == parent_pid)
+ guac_timestamp_msleep(GUACD_TIMEOUT);
+
+ kill(getpid(), SIGTERM);
+
+ return NULL;
+
+}
+#endif
+
+int guacd_proc_config_open_pipe(int config_pipe[2]) {
+
+#ifdef HAVE_PIPE2
+ if (pipe2(config_pipe, O_CLOEXEC)) {
+#else
+ if (pipe(config_pipe)) {
+#endif
+ /* The caller logs this failure */
+ return 1;
+ }
+#ifndef HAVE_PIPE2
+ fcntl(config_pipe[0], F_SETFD, FD_CLOEXEC);
+ fcntl(config_pipe[1], F_SETFD, FD_CLOEXEC);
+#endif
+
+ return 0;
+
+}
+
+int guacd_proc_config_write(int fd, const guacd_proc_config* config) {
+
+ size_t remaining = sizeof(guacd_proc_config);
+ const unsigned char* buffer = (const unsigned char*) config;
+
+ do {
+
+ ssize_t written;
+ GUAC_RETRY_EINTR(written, write(fd, buffer, remaining));
+
+ if (written <= 0)
+ return 1;
+
+ buffer += written;
+ remaining -= written;
+
+ } while (remaining > 0);
+
+ return 0;
+
+}
+
+void guacd_connection_process(int argc, char** argv) {
+
+ /* Logging must be reinitialized (fresh process image) */
+ openlog(GUACD_LOG_NAME, LOG_PID, LOG_DAEMON);
+
+ if (signal(SIGALRM, SIG_DFL) == SIG_ERR) {
+ guacd_log(GUAC_LOG_WARNING, "Could not set handler for SIGALRM to "
+ "default. The startup deadline of this connection process "
+ "may not be enforced.");
+ }
+
+ if (signal(SIGTERM, SIG_DFL) == SIG_ERR) {
+ guacd_log(GUAC_LOG_WARNING, "Could not set handler for SIGTERM to "
+ "default. This connection process may not terminate when "
+ "requested.");
+ }
+
+ sigset_t vital_signals;
+ sigemptyset(&vital_signals);
+ sigaddset(&vital_signals, SIGALRM);
+ sigaddset(&vital_signals, SIGTERM);
+ sigaddset(&vital_signals, SIGINT);
+ sigaddset(&vital_signals, SIGCHLD);
+ sigprocmask(SIG_UNBLOCK, &vital_signals, NULL);
+
+ /* Until under the supervision of the watchdog, bound startup via SIGALRM */
+ alarm(GUACD_SEC_TIMEOUT);
+
+ /* Do not create a connection process that is not subject to the failsafe */
+ if (getpgrp() != getpid() && setpgid(0, 0)) {
+ guacd_log(GUAC_LOG_ERROR, "Connection process is not the leader of "
+ "its own process group and cannot become one. Refusing to "
+ "create a connection process which cannot be forcibly "
+ "terminated.");
+ exit(EXIT_FAILURE);
+ }
+
+ /* Ignore SIGPIPE (writes to a closed socket/pipe should not terminate the
+ * process) */
+ if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
+ guacd_log(GUAC_LOG_INFO, "Could not set handler for SIGPIPE to "
+ "ignore. SIGPIPE may cause termination of this connection "
+ "process.");
+ }
+
+ /* Ignore SIGCHLD (automatically reap children on termination) */
+ if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) {
+ guacd_log(GUAC_LOG_INFO, "Could not set handler for SIGCHLD to "
+ "ignore. Terminated children of this connection process may "
+ "remain in the process table.");
+ }
+
+ /* Where supported, request that the OS dispatch SIGTERM upon the death of
+ * the parent guacd process. Where NOT supported, this is handled later
+ * through checking the parent PID (see usage of guacd_parent_watch_thread()
+ * below). */
+
+#if defined(PR_SET_PDEATHSIG)
+ prctl(PR_SET_PDEATHSIG, SIGTERM);
+#elif defined(PROC_PDEATHSIG_CTL)
+ procctl(P_PID, getpid(), PROC_PDEATHSIG_CTL, &(int) { SIGTERM });
+#endif
+
+ /* Copy the protocol out of the process name before
+ * guac_process_title_set() repurposes that memory */
+ char* protocol = guac_strdup(argv[0] + strlen(GUACD_PROC_NAME_PREFIX));
+
+ /* Read configuration from dedicated setup pipe */
+ guacd_proc_config config;
+ size_t config_length = 0;
+ ssize_t read_length;
+ do {
+ GUAC_RETRY_EINTR(read_length, read(GUACD_PROC_CONFIG_FD,
+ (char*) &config + config_length,
+ sizeof(config) - config_length));
+ if (read_length > 0)
+ config_length += read_length;
+ } while (read_length > 0 && config_length < sizeof(config));
+
+ /* Restore close-on-exec on the socket to the parent */
+ fcntl(GUACD_PROC_SOCKET_FD, F_SETFD, FD_CLOEXEC);
+
+ /* Nothing can be served without a complete configuration */
+ if (protocol == NULL || read_length < 0 || config_length != sizeof(config)
+ || config.connection_id[0] == '\0') {
+ guacd_log(GUAC_LOG_ERROR, "Connection process was started without a "
+ "usable protocol and configuration.");
+ exit(EXIT_FAILURE);
+ }
+
+ /* Ensure the received ID is terminated regardless of its contents */
+ config.connection_id[sizeof(config.connection_id) - 1] = '\0';
+
+ /* Re-arm the startup timeout alarm with what remains of the deadline. If
+ * that deadline has already passed, the parent is no longer waiting. */
+ guac_timestamp startup_remaining = config.startup_deadline - guac_timestamp_current();
+ if (startup_remaining <= 0) {
+ guacd_log(GUAC_LOG_ERROR, "Connection process did not start within "
+ "its startup deadline.");
+ exit(EXIT_FAILURE);
+ }
+
+ /* Round up to nearest whole second (a value of 0 disables the alarm) */
+ guac_timestamp startup_seconds = (startup_remaining + 999) / 1000;
+ GUAC_ASSERT(startup_seconds <= GUACD_SEC_TIMEOUT);
+
+ alarm((unsigned int) startup_seconds);
+
+ /* Arrival of the per-connection child process (THIS process) is announced to
+ * the parent guacd by closing this descriptor */
+ close(GUACD_PROC_CONFIG_FD);
+
+ /* The parent may already have died (before the requested signal could take
+ * effect), in which case this process has been reparented */
+ if (getppid() != config.parent_pid) {
+ guacd_log(GUAC_LOG_ERROR, "Parent guacd process terminated before "
+ "the connection could be served.");
+ exit(EXIT_FAILURE);
+ }
+
+#if !GUACD_HAVE_PDEATHSIG
+ /* Poll for the death of the parent from here on */
+ pthread_t parent_watch_thread;
+ if (pthread_create(&parent_watch_thread, NULL, guacd_parent_watch_thread,
+ (void*) (intptr_t) config.parent_pid)) {
+ guacd_log(GUAC_LOG_ERROR, "Unable to start a thread to watch for "
+ "the death of the parent guacd process. Refusing to serve a "
+ "connection which could outlive guacd.");
+ exit(EXIT_FAILURE);
+ }
+ pthread_detach(parent_watch_thread);
+#endif
+
+ /* Match the log verbosity configured for the parent daemon */
+ guacd_log_level = config.log_level;
+
+ /* Build the process entry and its guac_client */
+ guacd_proc* proc = guac_mem_zalloc(sizeof(guacd_proc));
+ if (proc == NULL) {
+ guacd_log(GUAC_LOG_ERROR, "Unable to allocate connection process.");
+ exit(EXIT_FAILURE);
+ }
+
+ proc->pid = 0;
+ proc->fd_socket = GUACD_PROC_SOCKET_FD;
+
+ proc->client = guac_client_alloc();
+ if (proc->client == NULL) {
+ guacd_log_guac_error(GUAC_LOG_ERROR, "Unable to create client");
+ exit(EXIT_FAILURE);
+ }
+
+ /* Adopt the connection identifier already assigned by the parent */
+ guac_mem_free(proc->client->connection_id);
+ proc->client->connection_id = guac_strdup(config.connection_id);
+ if (proc->client->connection_id == NULL) {
+ guacd_log(GUAC_LOG_ERROR, "Unable to allocate connection identifier.");
+ exit(EXIT_FAILURE);
+ }
+
+ proc->client->log_handler = guacd_client_log;
+
+ /* Serve the connection */
+ guacd_exec_proc(proc, protocol);
+ GUAC_ASSERT(0); /* guacd_exec_proc() does not return */
+
+}
diff --git a/src/guacd/proc-child.h b/src/guacd/proc-child.h
new file mode 100644
index 00000000..0ed22ea2
--- /dev/null
+++ b/src/guacd/proc-child.h
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#ifndef GUACD_PROC_CHILD_H
+#define GUACD_PROC_CHILD_H
+
+#include <guacamole/timestamp-types.h>
+
+#include <sys/types.h>
+
+/**
+ * The prefix of the process name (argv[0]) with which guacd_create_proc()
+ * re-execs guacd to serve a single connection. A process whose argv[0] begins
+ * with this prefix serves the protocol named by the remainder of argv[0].
+ */
+#define GUACD_PROC_NAME_PREFIX "guacd-connection-"
+
+/**
+ * The fixed file descriptor that the per-connection child process inherits as
+ * the socket to the parent guacd process.
+ */
+#define GUACD_PROC_SOCKET_FD 3
+
+/**
+ * The fixed file descriptor that the per-connection child process inherits as
+ * the means of receiving its configuration (a single guacd_proc_config
+ * structure). The parent observes arrival of the child process via the child
+ * reading that configuration and closing this file descriptor.
+ *
+ * @see guacd_proc_config
+ */
+#define GUACD_PROC_CONFIG_FD 4
+
+/**
+ * The configuration of a per-connection process, received from the parent guacd
+ * process as the raw structure's bytes written over GUACD_PROC_CONFIG_FD.
+ */
+typedef struct guacd_proc_config {
+
+ /**
+ * The maximum log level to be logged by the child process. This value is
+ * copied from guacd.
+ */
+ int log_level;
+
+ /**
+ * The process ID of the parent guacd process.
+ */
+ pid_t parent_pid;
+
+ /**
+ * The connection ID of the connection being served by this process.
+ */
+ char connection_id[256];
+
+ /**
+ * The time after which the connection process must have reached the point
+ * of supervising itself. If the connection process fails to complete
+ * startup by this point in time, the connection process is presumed
+ * to be malfunctioning and is killed.
+ */
+ guac_timestamp startup_deadline;
+
+} guacd_proc_config;
+
+/**
+ * Opens the dedicated pipe that the parent guacd process uses to send a
+ * connection process its configuration information. If the pipe cannot be
+ * opened, a non-zero value is returned and errno is set appropriately.
+ *
+ * @param config_pipe
+ * A two-element array to receive the read and write ends of the pipe,
+ * as produced by pipe().
+ *
+ * @return
+ * Zero if the pipe was successfully opened, non-zero otherwise.
+ */
+int guacd_proc_config_open_pipe(int config_pipe[2]);
+
+/**
+ * Writes the given configuration to the given file descriptor. It is expected
+ * that the file descriptor provided will be the write end of the pipe opened by
+ * guacd_proc_config_open_pipe(). If the configuration cannot be written,
+ * non-zero is returned and errno is set appropriately.
+ *
+ * @see guacd_proc_config_open_pipe()
+ *
+ * @param fd
+ * The file descriptor to write the configuration to.
+ *
+ * @param config
+ * The configuration to provide to the connection process.
+ *
+ * @return
+ * Zero if the configuration was sent, non-zero otherwise.
+ */
+int guacd_proc_config_write(int fd, const guacd_proc_config* config);
+
+/**
+ * Serves a single connection as a re-exec'd per-connection child process,
+ * taking the protocol from the suffix of argv[0] (see GUACD_PROC_NAME_PREFIX)
+ * and any other configuration information from GUACD_PROC_CONFIG_FD.
+ *
+ * This is the entry point for the process image created by guacd_create_proc()
+ * and does not return.
+ *
+ * @param argc
+ * The number of arguments within argv, exactly as passed to main().
+ *
+ * @param argv
+ * The arguments received by the child process, exactly as passed to main().
+ */
+void guacd_connection_process(int argc, char** argv);
+
+#endif
+
diff --git a/src/guacd/proc-map.c b/src/guacd/proc-map.c
index b1773336..f1e8f85d 100644
--- a/src/guacd/proc-map.c
+++ b/src/guacd/proc-map.c
@@ -22,8 +22,11 @@
#include "proc-map.h"
#include <guacamole/client.h>
+#include <guacamole/error.h>
#include <guacamole/mem.h>
+#include <errno.h>
+#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
@@ -43,6 +46,11 @@ typedef struct guacd_proc_map_entry {
*/
guac_common_list_element* element;
+ /**
+ * Whether users may still be routed to this process.
+ */
+ int routable;
+
} guacd_proc_map_entry;
/**
@@ -167,6 +175,7 @@ int guacd_proc_map_add(guacd_proc_map* map, guacd_proc* proc) {
guac_common_list_unlock(map->processes);
entry->proc = proc;
+ entry->routable = 1;
guac_common_list_add(bucket, entry);
guac_common_list_unlock(bucket);
@@ -180,9 +189,7 @@ int guacd_proc_map_add(guacd_proc_map* map, guacd_proc* proc) {
}
-guacd_proc* guacd_proc_map_retrieve(guacd_proc_map* map, const char* id) {
-
- guacd_proc* proc;
+int guacd_proc_map_open(guacd_proc_map* map, const char* id) {
guac_common_list* bucket = __guacd_proc_find_bucket(map, id);
guac_common_list_element* found;
@@ -194,19 +201,60 @@ guacd_proc* guacd_proc_map_retrieve(guacd_proc_map* map, const char* id) {
/* If no such element, fail */
if (found == NULL) {
guac_common_list_unlock(bucket);
- return NULL;
+ guac_error = GUAC_STATUS_NOT_FOUND;
+ guac_error_message = "No such connection";
+ return -1;
+ }
+
+ /* Consider unroutable connections non-existent for new attempts to join */
+ guacd_proc_map_entry* entry = (guacd_proc_map_entry*) found->data;
+ if (!entry->routable) {
+ guac_common_list_unlock(bucket);
+ guac_error = GUAC_STATUS_NOT_FOUND;
+ guac_error_message = "No such connection";
+ return -1;
}
- proc = ((guacd_proc_map_entry*) found->data)->proc;
+ guacd_proc* proc = entry->proc;
+ int fd = fcntl(proc->fd_socket, F_DUPFD_CLOEXEC, 0);
+ /* errno may be clobbered by guac_common_list_unlock() */
+ int dup_errno = errno;
guac_common_list_unlock(bucket);
- return proc;
+
+ if (fd < 0) {
+ guac_error = GUAC_STATUS_SEE_ERRNO;
+ guac_error_message = "Error duplicating socket of connection process";
+ errno = dup_errno;
+ return -1;
+ }
+
+ return fd;
}
-guacd_proc* guacd_proc_map_remove(guacd_proc_map* map, const char* id) {
+int guacd_proc_map_stop_routing(guacd_proc_map* map, const char* id) {
- guacd_proc* proc;
+ guac_common_list* bucket = __guacd_proc_find_bucket(map, id);
+
+ /* Retrieve corresponding element, if any */
+ guac_common_list_lock(bucket);
+ guac_common_list_element* found = __guacd_proc_find(bucket, id);
+
+ /* If no such element, fail */
+ if (found == NULL) {
+ guac_common_list_unlock(bucket);
+ return 1;
+ }
+
+ ((guacd_proc_map_entry*) found->data)->routable = 0;
+
+ guac_common_list_unlock(bucket);
+ return 0;
+
+}
+
+int guacd_proc_map_remove(guacd_proc_map* map, const char* id) {
guac_common_list* bucket = __guacd_proc_find_bucket(map, id);
guac_common_list_element* found;
@@ -218,7 +266,7 @@ guacd_proc* guacd_proc_map_remove(guacd_proc_map* map, const char* id) {
/* If no such element, fail */
if (found == NULL) {
guac_common_list_unlock(bucket);
- return NULL;
+ return 1;
}
guacd_proc_map_entry* entry = (guacd_proc_map_entry*) found->data;
@@ -228,13 +276,12 @@ guacd_proc* guacd_proc_map_remove(guacd_proc_map* map, const char* id) {
guac_common_list_remove(map->processes, entry->element);
guac_common_list_unlock(map->processes);
- proc = entry->proc;
guac_common_list_remove(bucket, found);
free (entry);
guac_common_list_unlock(bucket);
- return proc;
+ return 0;
}
diff --git a/src/guacd/proc-map.h b/src/guacd/proc-map.h
index 518b9ef3..d696603d 100644
--- a/src/guacd/proc-map.h
+++ b/src/guacd/proc-map.h
@@ -93,25 +93,44 @@ void guacd_proc_map_free(guacd_proc_map* map);
int guacd_proc_map_add(guacd_proc_map* map, guacd_proc* proc);
/**
- * Retrieves the client process having the client with the given ID, or NULL if
- * no such process is stored.
+ * Locates the client process having the client with the given ID, returning a
+ * new (duplicate) file descriptor that joining users' sockets may be sent over.
+ * The returned file descriptor must eventually be closed. If there is no such
+ * process, or a new file descriptor cannot be obtained, -1 is returned and
+ * guac_error is set appropriately.
*
* @param map
- * The map from which to retrieve the process associated with the client
+ * The map from which to locate the process associated with the client
* having the given ID.
*
* @param id
- * The ID of the client whose process should be retrieved.
+ * The ID of the client whose process should be located.
*
* @return
- * The process associated with the client having the given ID, or NULL if
- * no such process exists.
+ * A new file descriptor that joining users' socket file descriptors may be
+ * sent over, or -1 if the file descriptor could not be obtained.
*/
-guacd_proc* guacd_proc_map_retrieve(guacd_proc_map* map, const char* id);
+int guacd_proc_map_open(guacd_proc_map* map, const char* id);
/**
- * Removes the client process having the client with the given ID, returning
- * the corresponding process. If no such process exists, NULL is returned.
+ * Stops users being routed to the process serving the connection having the
+ * given ID. The process will no longer be accessible via guacd_proc_map_open(),
+ * but will still be represented within the map.
+ *
+ * @param map
+ * The map containing the process which should stop accepting users.
+ *
+ * @param id
+ * The ID of the client whose process should stop accepting users.
+ *
+ * @return
+ * Zero if routing for the process associated with the client having the
+ * given ID has been stopped, non-zero if no such process exists.
+ */
+int guacd_proc_map_stop_routing(guacd_proc_map* map, const char* id);
+
+/**
+ * Removes the client process having the client with the given ID.
*
* @param map
* The map from which to remove the process associated with the client
@@ -121,10 +140,10 @@ guacd_proc* guacd_proc_map_retrieve(guacd_proc_map* map, const char* id);
* The ID of the client whose process should be removed.
*
* @return
- * The process associated with the client having the given ID which has now
- * been removed from the given map, or NULL if no such process exists.
+ * Zero if the process associated with the client having the given ID has
+ * been removed from the given map, non-zero if no such process exists.
*/
-guacd_proc* guacd_proc_map_remove(guacd_proc_map* map, const char* id);
+int guacd_proc_map_remove(guacd_proc_map* map, const char* id);
/**
* A callback function that will be invoked with every guacd_proc stored
diff --git a/src/guacd/proc.c b/src/guacd/proc.c
index 15a1dd54..699e9ffa 100644
--- a/src/guacd/proc.c
+++ b/src/guacd/proc.c
@@ -20,6 +20,7 @@
#include "log.h"
#include "move-fd.h"
#include "proc.h"
+#include "proc-child.h"
#include "proc-map.h"
#include <guacamole/client.h>
@@ -30,31 +31,40 @@
#include <guacamole/proctitle.h>
#include <guacamole/protocol.h>
#include <guacamole/socket.h>
+#include <guacamole/string.h>
+#include <guacamole/timestamp.h>
#include <guacamole/user.h>
-#include <dirent.h>
#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
-#include <syslog.h>
#include <unistd.h>
-#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
+#include <sys/stat.h>
#include <sys/wait.h>
+#ifdef __FreeBSD__
+#include <sys/sysctl.h>
+#endif
+
+extern char** environ;
+
/**
* Parameters for the user thread.
*/
typedef struct guacd_user_thread_params {
/**
- * The process being joined.
+ * The client of the connection being joined.
*/
- guacd_proc* proc;
+ guac_client* client;
/**
* The file descriptor of the joining user's socket.
@@ -87,14 +97,44 @@ static void* guacd_user_thread(void* data) {
guac_thread_name_set("user-conn");
guacd_user_thread_params* params = (guacd_user_thread_params*) data;
- guacd_proc* proc = params->proc;
- guac_client* client = proc->client;
+ guac_client* client = params->client;
+
+ /* Bound how long a read from or write to this user may block (a full kernel
+ * buffer may otherwise block cleanup) */
+ if (setsockopt(params->fd, SOL_SOCKET, SO_SNDTIMEO,
+ &GUACD_TIMEOUT_TIMEVAL, sizeof(GUACD_TIMEOUT_TIMEVAL)))
+ guacd_log(GUAC_LOG_WARNING, "Unable to set write timeout on user "
+ "socket: %s. A user whose connection is blocked may delay "
+ "cleanup of this connection until it is otherwise closed.",
+ strerror(errno));
+
+ if (setsockopt(params->fd, SOL_SOCKET, SO_RCVTIMEO,
+ &GUACD_TIMEOUT_TIMEVAL, sizeof(GUACD_TIMEOUT_TIMEVAL)))
+ guacd_log(GUAC_LOG_WARNING, "Unable to set read timeout on user "
+ "socket: %s.", strerror(errno));
/* Get guac_socket for user's file descriptor */
guac_socket* socket = guac_socket_open(params->fd);
- if (socket == NULL)
+ if (socket == NULL) {
+
+ int owner = params->owner;
+ close(params->fd);
+ guac_mem_free(params);
+
+ if (owner) {
+ guac_client_stop(client);
+ guacd_log_guac_error(GUAC_LOG_ERROR, "Unable to create the "
+ "connection owner's socket. Stopping connection.");
+ }
+ else
+ guacd_log_guac_error(GUAC_LOG_ERROR, "Unable to create the "
+ "joining user's socket. The user has been dropped.");
+
+ guac_client_watchdog_notify_user_left(client);
return NULL;
+ }
+
/* Create skeleton user */
guac_user* user = guac_user_alloc();
user->socket = socket;
@@ -104,16 +144,17 @@ static void* guacd_user_thread(void* data) {
/* Handle user connection from handshake until disconnect/completion */
guac_user_handle_connection(user, GUACD_USEC_TIMEOUT);
- /* Stop client and prevent future users if all users are disconnected */
- if (client->connected_users == 0) {
- guacd_log(GUAC_LOG_INFO, "Last user of connection \"%s\" disconnected", client->connection_id);
- guacd_proc_stop(proc);
- }
-
/* Clean up */
+ guac_client_watchdog_notify_operation_start(client);
guac_socket_free(socket);
guac_user_free(user);
guac_mem_free(params);
+ guac_client_watchdog_notify_operation_end(client);
+
+ /* NOTE: The user count reaching zero here will begin concurrent teardown of
+ * the client, thus no further calls referencing the client may be made after
+ * this */
+ guac_client_watchdog_notify_user_left(client);
return NULL;
@@ -137,14 +178,63 @@ static void* guacd_user_thread(void* data) {
*/
static void guacd_proc_add_user(guacd_proc* proc, int fd, int owner) {
+ guac_client* client = proc->client;
+
guacd_user_thread_params* params = guac_mem_alloc(sizeof(guacd_user_thread_params));
- params->proc = proc;
+ if (params == NULL) {
+
+ close(fd);
+
+ if (owner) {
+ guacd_proc_stop(proc);
+ guacd_log(GUAC_LOG_ERROR, "Unable to allocate the parameters of "
+ "the connection owner's thread. Stopping connection.");
+ }
+ else
+ guacd_log(GUAC_LOG_ERROR, "Unable to allocate the parameters of "
+ "a joining user's thread. The user has been dropped.");
+
+ return;
+
+ }
+
+ params->client = client;
params->fd = fd;
params->owner = owner;
+ /* Notify that a new user is starting, but bail out if this is refused due to
+ * all users having already left */
+ if (guac_client_watchdog_notify_user_joined(client)) {
+ close(fd);
+ guac_mem_free(params);
+ guacd_log(GUAC_LOG_INFO, "Joining user dropped - connection has already ended.");
+ return;
+ }
+
/* Start user thread */
pthread_t user_thread;
- pthread_create(&user_thread, NULL, guacd_user_thread, params);
+ int result = pthread_create(&user_thread, NULL, guacd_user_thread, params);
+ if (result) {
+
+ close(fd);
+ guac_mem_free(params);
+
+ if (owner) {
+ guacd_proc_stop(proc);
+ guacd_log(GUAC_LOG_ERROR, "Unable to start thread for "
+ "connection owner: %s. Stopping connection.",
+ strerror(result));
+ }
+ else
+ guacd_log(GUAC_LOG_ERROR, "Unable to start thread for "
+ "joining user: %s. The user has been dropped.",
+ strerror(result));
+
+ guac_client_watchdog_notify_user_left(client);
+ return;
+
+ }
+
pthread_detach(user_thread);
}
@@ -166,143 +256,41 @@ static void guacd_kill_current_proc_group() {
}
/**
- * The current status of a background attempt to free a guac_client instance.
+ * The watchdog of the current per-connection process. This value is assigned
+ * only once and only within a per-connection process. This reference to the
+ * watchdog exists as a means of requesting termination from within a signal
+ * handler.
+ *
+ * @see signal_stop_handler()
*/
-typedef struct guacd_client_free {
-
- /**
- * The guac_client instance being freed.
- */
- guac_client* client;
-
- /**
- * The condition which is signalled whenever changes are made to the
- * completed flag. The completed flag only changes from zero (not yet
- * freed) to non-zero (successfully freed).
- */
- pthread_cond_t completed_cond;
-
- /**
- * Mutex which must be acquired before any changes are made to the
- * completed flag.
- */
- pthread_mutex_t completed_mutex;
-
- /**
- * Whether the guac_client has been successfully freed. Initially, this
- * will be zero, indicating that the free operation has not yet been
- * attempted. If the client is eventually successfully freed, this will be
- * set to a non-zero value. Changes to this flag are signalled through
- * the completed_cond condition.
- */
- int completed;
-
-} guacd_client_free;
+static guac_client_watchdog* guacd_proc_watchdog = NULL;
/**
- * Thread which frees a given guac_client instance in the background. If the
- * free operation succeeds, a flag is set on the provided structure, and the
- * change in that flag is signalled with a pthread condition.
- *
- * At the time this function is provided to a pthread_create() call, the
- * completed flag of the associated guacd_client_free structure MUST be
- * initialized to zero, the pthread mutex and condition MUST both be
- * initialized, and the client pointer must point to the guac_client being
- * freed.
+ * Thread which waits for teardown of the given process' connection to begin.
+ * Once teardown has begun, this thread shuts down the internal socket of the
+ * guacd_proc to unblock that process' main loop and allow cleanup to proceed.
*
* @param data
- * A pointer to a guacd_client_free structure describing the free
- * operation.
+ * The guacd_proc whose main loop should be unblocked.
*
* @return
* Always NULL.
*/
-static void* guacd_client_free_thread(void* data) {
+static void* guacd_teardown_watch_thread(void* data) {
- /* Thread name client-free: frees a guac_client in the background,
- * bounded by a timeout in case the free handler hangs. */
- guac_thread_name_set("client-free");
+ /* Thread name teardown-watch: watches for client teardown to begin and wakes
+ * the main loop. */
+ guac_thread_name_set("teardown-watch");
- guacd_client_free* free_operation = (guacd_client_free*) data;
+ guacd_proc* proc = (guacd_proc*) data;
- /* Attempt to free client (this may never return if the client is
- * malfunctioning) */
- guac_client_free(free_operation->client);
-
- /* Signal that the client was successfully freed */
- pthread_mutex_lock(&free_operation->completed_mutex);
- free_operation->completed = 1;
- pthread_cond_broadcast(&free_operation->completed_cond);
- pthread_mutex_unlock(&free_operation->completed_mutex);
+ guac_client_watchdog_await_teardown(proc->watchdog);
+ shutdown(proc->fd_socket, SHUT_RDWR);
return NULL;
}
-/**
- * Attempts to free the given guac_client, restricting the time taken by the
- * free handler of the guac_client to a finite number of seconds. If the free
- * handler does not complete within the time allotted, this function returns
- * and the intended free operation is left in an undefined state.
- *
- * @param client
- * The guac_client instance to free.
- *
- * @param timeout
- * The maximum amount of time to wait for the guac_client to be freed,
- * in seconds.
- *
- * @return
- * Zero if the guac_client was successfully freed within the time allotted,
- * non-zero otherwise.
- */
-static int guacd_timed_client_free(guac_client* client, int timeout) {
-
- pthread_t client_free_thread;
-
- guacd_client_free free_operation = {
- .client = client,
- .completed_cond = PTHREAD_COND_INITIALIZER,
- .completed_mutex = PTHREAD_MUTEX_INITIALIZER,
- .completed = 0
- };
-
- /* Get current time */
- struct timeval current_time;
- if (gettimeofday(¤t_time, NULL))
- return 1;
-
- /* Calculate exact time that the free operation MUST complete by */
- struct timespec deadline = {
- .tv_sec = current_time.tv_sec + timeout,
- .tv_nsec = current_time.tv_usec * 1000
- };
-
- /* The mutex associated with the pthread conditional and flag MUST be
- * acquired before attempting to wait for the condition */
- if (pthread_mutex_lock(&free_operation.completed_mutex))
- return 1;
-
- /* Free the client in a separate thread, so we can time the free operation */
- if (!pthread_create(&client_free_thread, NULL,
- guacd_client_free_thread, &free_operation)) {
-
- /* Wait a finite amount of time for the free operation to finish */
- (void) pthread_cond_timedwait(&free_operation.completed_cond,
- &free_operation.completed_mutex, &deadline);
- }
-
- (void) pthread_mutex_unlock(&free_operation.completed_mutex);
-
- /* Return status of free operation */
- return !free_operation.completed;
-}
-
-/**
- * A reference to the current guacd process.
- */
-guacd_proc* guacd_proc_self = NULL;
-
/**
* A signal handler that will be invoked when a signal is caught telling this
* guacd process to immediately exit.
@@ -313,43 +301,45 @@ guacd_proc* guacd_proc_self = NULL;
*/
static void signal_stop_handler(int signal) {
- /* Stop the current guacd proc */
- guacd_proc_stop(guacd_proc_self);
+ /* NOTE: Requesting the process to stop is done via the watchdog here, as
+ * guacd_proc_stop() is not async-signal-safe */
+ guac_client_watchdog_stop_connection(guacd_proc_watchdog);
}
-/**
- * Starts protocol-specific handling on the given process by loading the client
- * plugin for that protocol. This function does NOT return. It initializes the
- * process with protocol-specific handlers and then runs until the guacd_proc's
- * fd_socket is closed, adding any file descriptors received along fd_socket as
- * new users.
- *
- * @param proc
- * The process that any new users received along fd_socket should be added
- * to (after the process has been initialized for the given protocol).
- *
- * @param protocol
- * The protocol to initialize the given process for.
- */
-static void guacd_exec_proc(guacd_proc* proc, const char* protocol) {
+void guacd_exec_proc(guacd_proc* proc, const char* protocol) {
- int result = 1;
-
- /* Label the new child process. guac_process_title_set() also writes the
- * main thread's comm, which is the current thread here, so a separate
- * guac_thread_name_set() call would just duplicate the work. */
+ /* Label the new child process */
guac_process_title_set(protocol);
- /* Set process group ID to match PID */
- if (setpgid(0, 0)) {
- guacd_log(GUAC_LOG_ERROR, "Cannot set PGID for connection process: %s",
- strerror(errno));
- goto cleanup_process;
+ /* Start watchdog as early as possible, before loading the plugin */
+ guac_client* client = proc->client;
+ proc->watchdog = guacd_proc_watchdog = guac_client_watchdog_start(client,
+ GUACD_TIMEOUT, GUACD_CLEANUP_TIMEOUT);
+ if (proc->watchdog == NULL) {
+ guacd_log(GUAC_LOG_ERROR, "Unable to start the watchdog thread. "
+ "Refusing to serve the connection unsupervised.");
+
+ /* No watchdog supervises this path, so the exit must not block */
+ _exit(EXIT_FAILURE);
+ }
+
+ guac_client_watchdog_notify_operation_start(client);
+ int awaiting_first_user = 1;
+
+ /* The startup deadline is no longer needed - the watchdog takes over from
+ * here */
+ alarm(0);
+
+ /* Unblock recvmsg() in the main loop once teardown starts */
+ pthread_t teardown_watch_thread;
+ if (pthread_create(&teardown_watch_thread, NULL, guacd_teardown_watch_thread, proc)) {
+ guacd_log(GUAC_LOG_ERROR, "Unable to start the teardown watch thread. "
+ "Refusing to serve the connection unsupervised.");
+ _exit(EXIT_FAILURE);
}
/* Init client for selected protocol */
- guac_client* client = proc->client;
if (guac_client_load_plugin(client, protocol)) {
/* Log error */
@@ -369,20 +359,41 @@ static void guacd_exec_proc(guacd_proc* proc, const char* protocol) {
/* Enable keep alive on the broadcast socket */
guac_socket_require_keep_alive(client->socket);
- guacd_proc_self = proc;
-
/* Clean up and exit if SIGINT or SIGTERM signals are caught */
struct sigaction signal_stop_action = {
.sa_handler = signal_stop_handler,
- /* Restart system calls interrupted by signal delivery */
.sa_flags = SA_RESTART
};
sigaction(SIGINT, &signal_stop_action, NULL);
sigaction(SIGTERM, &signal_stop_action, NULL);
/* Add each received file descriptor as a new user */
- int received_fd;
- while ((received_fd = guacd_recv_fd(proc->fd_socket)) != -1) {
+ for (;;) {
+
+ /* errno must describe this call and no earlier one */
+ errno = 0;
+
+ int received_fd = guacd_recv_fd(proc->fd_socket);
+ if (received_fd == -1) {
+
+ /* Being out of file descriptors prevents this user from joining but
+ * says nothing of those already served */
+ if (errno == EMFILE || errno == ENFILE) {
+ guacd_log(GUAC_LOG_ERROR, "Unable to accept %s: %s",
+ owner ? "the owner of this connection" : "new user",
+ strerror(errno));
+ if (!owner)
+ continue;
+ }
+
+ break;
+
+ }
+
+ if (awaiting_first_user) {
+ guac_client_watchdog_notify_operation_end(client);
+ awaiting_first_user = 0;
+ }
guacd_proc_add_user(proc, received_fd, owner);
@@ -393,22 +404,26 @@ static void guacd_exec_proc(guacd_proc* proc, const char* protocol) {
cleanup_client:
+ if (awaiting_first_user)
+ guac_client_watchdog_notify_operation_end(client);
+
+ /* Connection has ended and we are preparing to free the client */
+ guac_client_watchdog_notify_teardown_start(proc->watchdog);
+ pthread_join(teardown_watch_thread, NULL);
+
/* Request client to stop/disconnect */
guac_client_stop(client);
- /* Attempt to free client cleanly */
- guacd_log(GUAC_LOG_DEBUG, "Requesting termination of client...");
- result = guacd_timed_client_free(client, GUACD_CLIENT_FREE_TIMEOUT);
+ /* Cancel outstanding I/O and wait for all users to be disconnected */
+ guac_socket_shutdown(client->socket);
+ guac_socket_shutdown(client->pending_socket);
+ guac_socket_shutdown(client->socket);
+ guac_client_watchdog_await_all_users(proc->watchdog);
- /* If client was unable to be freed, warn and forcibly kill */
- if (result) {
- guacd_log(GUAC_LOG_WARNING, "Client did not terminate in a timely "
- "manner. Forcibly terminating client and any child "
- "processes.");
- guacd_kill_current_proc_group();
- }
- else
- guacd_log(GUAC_LOG_DEBUG, "Client terminated successfully.");
+ /* The client can now safely be freed */
+ guacd_log(GUAC_LOG_DEBUG, "Requesting termination of client...");
+ guac_client_free(client);
+ guacd_log(GUAC_LOG_DEBUG, "Client terminated successfully.");
/* Verify whether children were all properly reaped */
pid_t child_pid;
@@ -430,132 +445,102 @@ cleanup_client:
guacd_kill_current_proc_group();
}
-cleanup_process:
-
/* Free up all internal resources outside the client */
close(proc->fd_socket);
+ guac_client_watchdog* watchdog = proc->watchdog;
guac_mem_free(proc);
- exit(result);
+ guac_client_watchdog_notify_teardown_end(watchdog);
+
+ /* Cannot risk exit() destructors running here, as a malfunctioning plugin
+ * may hold locks that prevent destruction from completing */
+ _exit(EXIT_SUCCESS);
}
-/**
- * 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 first
- * The first file descriptor in the range to be closed.
- *
- * @param last
- * The last file descriptor in the range to be closed.
- *
- * @return
- * 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_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);
+int guacd_open_exe(const char* invoked_path) {
+
+ int fd;
+
+#ifdef __FreeBSD__
+
+ /* FreeBSD provides the path of the running executable via sysctl, without
+ * requiring /proc (which is not mounted by default) */
+ char path[PATH_MAX];
+ size_t size = sizeof(path);
+ int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };
+ if (sysctl(mib, 4, path, &size, NULL, 0) == 0) {
+ fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd >= 0)
+ return fd;
+ }
+
#else
- return -1;
-#endif
-}
+ /* Leverage /proc on other platforms */
+ fd = open("/proc/self/exe", O_RDONLY | O_CLOEXEC);
+ if (fd >= 0)
+ return fd;
-/**
- * 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 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
- * STDERR_FILENO.
- */
-static void guacd_close_inherited_fds(int keep_fd) {
+#endif
- /* Force syslog to reopen its own descriptor as needed, rather than
- * continuing to write to whatever ends up occupying the one closed here */
- closelog();
+ /* Fall back to deriving from the filename used to start guacd */
+ if (invoked_path != NULL && strchr(invoked_path, '/') != NULL) {
- /* 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)) {
+ char resolved[PATH_MAX];
+ if (realpath(invoked_path, resolved) == NULL)
+ return -1;
- if (keep_fd <= STDERR_FILENO + 1)
- return;
+ fd = open(resolved, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -1;
- if (!guacd_close_fd_range(STDERR_FILENO + 1, keep_fd - 1))
- return;
+ /* The path must at least name a regular file */
+ struct stat exe_stat;
+ if (fstat(fd, &exe_stat) || !S_ISREG(exe_stat.st_mode)) {
+ close(fd);
+ errno = EACCES;
+ return -1;
+ }
- }
+ return fd;
- /* 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__
- DIR* open_fds = opendir("/proc/self/fd");
- if (open_fds != NULL) {
+ }
- int dir_fd = dirfd(open_fds);
+ return -1;
- struct dirent* entry;
- while ((entry = readdir(open_fds)) != NULL) {
+}
- /* 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)
- close(fd);
+void guacd_proc_free(guacd_proc* proc) {
- }
+ close(proc->fd_socket);
- closedir(open_fds);
- return;
+ if (proc->client != NULL)
+ guac_client_free(proc->client);
- }
-#endif
+ /* Reap the process and any remaining members of its process group */
+ if (proc->pid > 0) {
- /* 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;
+ pid_t reaped;
+ GUAC_RETRY_EINTR(reaped, waitpid(proc->pid, NULL, 0));
+ do {
+ GUAC_RETRY_EINTR(reaped, waitpid(-proc->pid, NULL, 0));
+ } while (reaped > 0);
- for (long fd = STDERR_FILENO + 1; fd < max_fd; fd++) {
- if (fd != keep_fd)
- close(fd);
}
+ guac_mem_free(proc);
+
}
-guacd_proc* guacd_create_proc(const char* protocol) {
+guacd_proc* guacd_create_proc(const char* protocol, int exe_fd) {
int sockets[2];
- /* Open UNIX socket pair */
- if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets) < 0) {
+ /* Open UNIX socket pair, close-on-exec. The child's end survives its
+ * re-exec only through its move onto GUACD_PROC_SOCKET_FD, which clears the
+ * flag. */
+ if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0, sockets) < 0) {
guacd_log(GUAC_LOG_ERROR, "Error opening socket pair: %s", strerror(errno));
return NULL;
}
@@ -563,6 +548,15 @@ guacd_proc* guacd_create_proc(const char* protocol) {
int parent_socket = sockets[0];
int child_socket = sockets[1];
+ /* Bound how long sending a user to the connection process may block (a
+ * blocked process does not receive, and the queue of users awaiting it is
+ * short) */
+ if (setsockopt(child_socket, SOL_SOCKET, SO_SNDTIMEO,
+ &GUACD_TIMEOUT_TIMEVAL, sizeof(GUACD_TIMEOUT_TIMEVAL)))
+ guacd_log(GUAC_LOG_WARNING, "Unable to set send timeout on internal "
+ "socket of connection process: %s. A blocked connection "
+ "process may delay users joining it.", strerror(errno));
+
/* Allocate process */
guacd_proc* proc = guac_mem_zalloc(sizeof(guacd_proc));
if (proc == NULL) {
@@ -571,87 +565,219 @@ guacd_proc* guacd_create_proc(const char* protocol) {
return NULL;
}
+ proc->fd_socket = child_socket;
+
/* Associate new client */
proc->client = guac_client_alloc();
if (proc->client == NULL) {
guacd_log_guac_error(GUAC_LOG_ERROR, "Unable to create client");
close(parent_socket);
- close(child_socket);
- guac_mem_free(proc);
+ guacd_proc_free(proc);
return NULL;
}
/* Init logging */
proc->client->log_handler = guacd_client_log;
+ int config_pipe[2];
+ if (guacd_proc_config_open_pipe(config_pipe)) {
+ guacd_log(GUAC_LOG_ERROR, "Error opening configuration pipe: %s",
+ strerror(errno));
+ close(parent_socket);
+ guacd_proc_free(proc);
+ return NULL;
+ }
+
+ /* Compose arguments before forking, as only async-signal-safe operations are
+ * permitted between fork and exec */
+ char process_name[128] = GUACD_PROC_NAME_PREFIX;
+ guac_strlcat(process_name, protocol, sizeof(process_name));
+
+ char* const child_argv[] = {
+ process_name,
+ NULL
+ };
+
/* Fork */
proc->pid = fork();
if (proc->pid < 0) {
guacd_log(GUAC_LOG_ERROR, "Cannot fork child process: %s", strerror(errno));
close(parent_socket);
- close(child_socket);
- guac_client_free(proc->client);
- guac_mem_free(proc);
+ close(config_pipe[0]);
+ close(config_pipe[1]);
+ guacd_proc_free(proc);
return NULL;
}
/* Child */
else if (proc->pid == 0) {
- /* Communicate with parent */
- proc->fd_socket = parent_socket;
- close(child_socket);
+ /* Establish own process group so the parent can signal the whole group
+ * by PID. Without one, the teardown timeout's kill(0, SIGKILL) would
+ * kill guacd itself. */
+ if (setpgid(0, 0))
+ _exit(EXIT_FAILURE);
- /* Discard descriptors belonging to unrelated connections */
- guacd_close_inherited_fds(proc->fd_socket);
+ /* Move the socket and our end of the config pipe onto their known, fixed
+ * descriptors (these were reserved at startup within main()) */
+ if (dup2(parent_socket, GUACD_PROC_SOCKET_FD) == -1
+ || dup2(config_pipe[0], GUACD_PROC_CONFIG_FD) == -1)
+ _exit(EXIT_FAILURE);
- /* Start protocol-specific handling */
- guacd_exec_proc(proc, protocol);
+ /* Re-exec as a per-connection process from the image pinned by the file
+ * descriptor during startup */
+ fexecve(exe_fd, child_argv, environ);
+
+ /* NOTE: We cannot log any failures here. We can reach here only if
+ * fexecve() above fails, which means any syslog-related locks that were
+ * inherited are still present and may be locked. */
+
+ _exit(EXIT_FAILURE);
}
/* Parent */
else {
- /* Communicate with child */
- proc->fd_socket = child_socket;
+ /* Establish the child's process group from this side too, as a group
+ * kill fails until the child runs its own setpgid(). Whichever side
+ * runs first succeeds. */
+ setpgid(proc->pid, proc->pid);
+
+ /* The child holds its own copies of the pipe and the other end of the
+ * socket pair */
+ close(config_pipe[0]);
close(parent_socket);
+ /* NOTE: The timed period for the connection process to reach
+ * self-supervision under its own watchdog thread begins now. */
+
+ guac_timestamp startup_deadline = guac_timestamp_current() + GUACD_TIMEOUT;
+
+ /* Provide the child with its configuration and identity over the
+ * dedicated setup pipe */
+
+ guacd_proc_config config = {
+ .log_level = guacd_log_level,
+ .parent_pid = getpid(),
+ .startup_deadline = startup_deadline
+ };
+
+ guac_strlcpy(config.connection_id, proc->client->connection_id,
+ sizeof(config.connection_id));
+
+ int send_failed = guacd_proc_config_write(config_pipe[1], &config);
+
+ /* Await the child's arrival, announced by closing its end of the
+ * configuration pipe. Nothing within the child can bound this, exec and
+ * loader preceding its code. */
+ int arrived = 0;
+ if (!send_failed) {
+ struct pollfd arrival = { .fd = config_pipe[1] };
+ for (;;) {
+
+ int remaining = (int) (startup_deadline - guac_timestamp_current());
+ if (remaining < 0)
+ remaining = 0;
+
+ arrived = poll(&arrival, 1, remaining);
+ if (arrived != -1 || errno != EINTR)
+ break;
+
+ }
+ }
+
+ /* The write end is no longer needed, whether or not the child arrived */
+ close(config_pipe[1]);
+
+ /* Disambiguate child arrival from child death (both result in the
+ * closure of the config pipe) */
+ int still_running = guacd_proc_is_running(proc);
+ if (arrived != 1 || !still_running) {
+
+ if (still_running) {
+
+ /* Forcibly kill still running process */
+ kill(-proc->pid, SIGKILL);
+ kill(proc->pid, SIGKILL);
+
+ if (send_failed)
+ guacd_log(GUAC_LOG_ERROR, "Unable to send the "
+ "configuration of a connection to the process "
+ "meant to serve it. That process has been "
+ "killed and the connection dropped.");
+ else
+ guacd_log(GUAC_LOG_ERROR, "Connection process did not "
+ "arrive at its own startup and has been "
+ "killed.");
+
+ }
+
+ /* Process never finished starting up */
+ else
+ guacd_log(GUAC_LOG_ERROR, "Connection process died during "
+ "its own startup.");
+
+ /* Prevent the caller from routing users to a process that cannot
+ * serve them */
+ guacd_proc_free(proc);
+
+ return NULL;
+
+ }
+
}
return proc;
}
+int guacd_proc_is_running(guacd_proc* proc) {
+
+ siginfo_t status;
+ memset(&status, 0, sizeof(status));
+
+ int result;
+ GUAC_RETRY_EINTR(result, waitid(P_PID, proc->pid, &status,
+ WEXITED | WNOHANG | WNOWAIT));
+
+ return !result && status.si_pid == 0;
+
+}
+
+void guacd_proc_await_exit(guacd_proc* proc) {
+
+ siginfo_t status;
+ memset(&status, 0, sizeof(status));
+
+ int result;
+ GUAC_RETRY_EINTR(result, waitid(P_PID, proc->pid, &status,
+ WEXITED | WNOWAIT));
+
+}
+
/**
- * Kill the provided child guacd process. This function must be called by the
- * parent process, and will block until all processes associated with the
- * child process have terminated.
+ * Requests termination of the provided child guacd process and everything it
+ * has spawned, returning as soon as that request has been made. This function
+ * must be called by the parent process.
*
* @param proc
- * The child guacd process to kill.
+ * The child guacd process to terminate.
*/
static void guacd_proc_kill(guacd_proc* proc) {
- /* Request orderly termination of process group */
- if (kill(-proc->pid, SIGTERM))
- guacd_log(GUAC_LOG_DEBUG, "Unable to request termination of "
- "client process: %s ", strerror(errno));
-
- /* Wait for all processes within process group to terminate */
- pid_t child_pid;
- while (1) {
- GUAC_RETRY_EINTR(child_pid, waitpid(-proc->pid, NULL, 0));
-
- if (child_pid <= 0)
- break;
-
- guacd_log(GUAC_LOG_DEBUG, "Child process %i of connection \"%s\" has terminated",
- child_pid, proc->client->connection_id);
+ /* Request orderly termination of the process group */
+ if (guacd_proc_is_running(proc)) {
+ if (kill(-proc->pid, SIGTERM))
+ guacd_log(GUAC_LOG_DEBUG, "Unable to request termination of "
+ "connection %s: %s", proc->client->connection_id,
+ strerror(errno));
}
- guacd_log(GUAC_LOG_DEBUG, "All child processes for connection \"%s\" have been terminated.",
- proc->client->connection_id);
+ /* Without a running connection process, there's nothing to pass on the
+ * termination signal - we must manually force with SIGKILL */
+ else
+ kill(-proc->pid, SIGKILL);
}
@@ -668,13 +794,4 @@ void guacd_proc_stop(guacd_proc* proc) {
/* Signal client to stop */
guac_client_stop(proc->client);
- /* Shutdown socket - in-progress recvmsg() will not fail otherwise */
- if (shutdown(proc->fd_socket, SHUT_RDWR) == -1)
- guacd_log(GUAC_LOG_ERROR, "Unable to shutdown internal socket for "
- "connection %s. Corresponding process may remain running but "
- "inactive.", proc->client->connection_id);
-
- /* Clean up our end of the socket */
- close(proc->fd_socket);
-
}
diff --git a/src/guacd/proc.h b/src/guacd/proc.h
index 4ee749f0..06cb4f1a 100644
--- a/src/guacd/proc.h
+++ b/src/guacd/proc.h
@@ -23,6 +23,7 @@
#include <guacamole/client.h>
#include <guacamole/parser.h>
+#include <sys/time.h>
#include <unistd.h>
/**
@@ -39,12 +40,27 @@
#define GUACD_USEC_TIMEOUT (GUACD_TIMEOUT*1000)
/**
- * The number of seconds to wait for any particular guac_client instance
- * to be freed following disconnect. If the free operation does not complete
- * within this period of time, the associated process will be forcibly
- * terminated.
+ * The number of seconds to wait for messages in any phase before timing out
+ * and closing the connection with an error. This is always equal to
+ * GUACD_TIMEOUT / 1000.
*/
-#define GUACD_CLIENT_FREE_TIMEOUT 5
+#define GUACD_SEC_TIMEOUT (GUACD_TIMEOUT/1000)
+
+/**
+ * The number of milliseconds to wait for messages in any phase before timing
+ * out and closing the connection with an error, as a struct timeval.
+ */
+#define GUACD_TIMEOUT_TIMEVAL ((struct timeval) { \
+ .tv_sec = GUACD_TIMEOUT / 1000, \
+ .tv_usec = (GUACD_TIMEOUT % 1000) * 1000 \
+})
+
+/**
+ * The number of milliseconds to wait for a connection process to be entirely
+ * cleaned up following disconnect. If the cleanup operation does not complete
+ * within this period of time, the process will be forcibly terminated.
+ */
+#define GUACD_CLEANUP_TIMEOUT 5000
/**
* Process information of the internal remote desktop client.
@@ -76,23 +92,109 @@ typedef struct guacd_proc {
*/
guac_client* client;
+ /**
+ * The watchdog of the client. If guac_client_watchdog_start() has not yet
+ * been called by the connection process, this will be NULL.
+ */
+ guac_client_watchdog* watchdog;
+
} guacd_proc;
+/**
+ * Returns whether the given connection process is still running.
+ *
+ * @param proc
+ * The connection process to test.
+ *
+ * @return
+ * Non-zero if the process is still running, zero if it has terminated or
+ * can no longer be observed.
+ */
+int guacd_proc_is_running(guacd_proc* proc);
+
+/**
+ * Waits (blocks) until the given connection process has terminated. No process
+ * is reaped by this function.
+ *
+ * @param proc
+ * The connection process to await the termination of.
+ */
+void guacd_proc_await_exit(guacd_proc* proc);
+
+/**
+ * Returns a file descriptor referring to the executable of the running guacd
+ * instance. It is intended that this file descriptor remain open for the
+ * lifetime of guacd, to ensure subprocesses can be reliably exec'd without
+ * breaking the ABI, even if guacd is deleted/upgraded while running. If the
+ * executable cannot be located, -1 is returned, and errno is set appropriately.
+ *
+ * @param invoked_path
+ * The path and name provided when guacd was invoked (argv[0]). This value
+ * is used as a fallback when the executable cannot be located by
+ * platform-specific means. If provided, this value must contain at least
+ * one directory reference (at least one "/") to be usable as a fallback,
+ * and will be resolved relative to the current working directory. This
+ * value may be NULL.
+ *
+ * @return
+ * A file descriptor referring to the running guacd executable, or -1 if the
+ * executable could not be located.
+ */
+int guacd_open_exe(const char* invoked_path);
+
/**
* Creates a new background process for handling the given protocol, returning
* a structure allowing communication with and monitoring of the process
* created. Within the child process, this function does not return - the
- * entire child process simply terminates instead.
+ * entire child process is replaced via exec() instead, serving the connection
+ * through guacd_connection_process().
*
* @param protocol
* The protocol for which this process is client being created.
*
+ * @param exe_fd
+ * A file descriptor referring to the guacd executable, as opened by
+ * guacd_open_exe().
+ *
* @return
* A newly-allocated process structure pointing to the file descriptor of
* the background process specific to the specified protocol, or NULL of
* the process could not be created.
*/
-guacd_proc* guacd_create_proc(const char* protocol);
+guacd_proc* guacd_create_proc(const char* protocol, int exe_fd);
+
+/**
+ * Frees the given process structure and any associated resources, including
+ * reaping the underlying process. The relevant process and its group MUST
+ * already have been terminated.
+ *
+ * @see guacd_proc_stop()
+ *
+ * @note
+ * This should be invoked only by the thread which created the guacd_proc,
+ * and only once no other thread may still hold a reference to that
+ * guacd_proc (it is no longer in the process map).
+ *
+ * @param proc
+ * The process structure to free.
+ */
+void guacd_proc_free(guacd_proc* proc);
+
+/**
+ * Starts protocol-specific handling on the given process by loading the client
+ * plugin for that protocol. This function does NOT return. It initializes the
+ * process with protocol-specific handlers and then runs until the guacd_proc's
+ * fd_socket is shut down or closed, adding any file descriptors received along
+ * fd_socket as new users.
+ *
+ * @param proc
+ * The process that any new users received along fd_socket should be added
+ * to (after the process has been initialized for the given protocol).
+ *
+ * @param protocol
+ * The protocol to initialize the given process for.
+ */
+void guacd_exec_proc(guacd_proc* proc, const char* protocol);
/**
* Signals the given process to stop accepting new users and clean up. This