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(&current_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