Commit 57d287ff for guacamole.apache.org
commit 57d287ff0513e51c7d6074dba7e3766f8b36a6e1
Author: Bradley Bennett <bbennett@keepersecurity.com>
Date: Thu May 28 22:32:57 2026 -0400
GUACAMOLE-2283: Runtime observability: add guacd process & thread naming.
diff --git a/src/guacd/connection.c b/src/guacd/connection.c
index c07d6643..7f5e7e86 100644
--- a/src/guacd/connection.c
+++ b/src/guacd/connection.c
@@ -30,6 +30,7 @@
#include <guacamole/mem.h>
#include <guacamole/parser.h>
#include <guacamole/plugin.h>
+#include <guacamole/proctitle.h>
#include <guacamole/protocol.h>
#include <guacamole/socket.h>
#include <guacamole/user.h>
@@ -106,6 +107,10 @@ static int __write_all(int fd, char* buffer, int length) {
*/
static void* guacd_connection_write_thread(void* data) {
+ /* Thread name conn-write: forwards data from the connected user to the
+ * connection's child process. */
+ guac_thread_name_set("conn-write");
+
guacd_connection_io_thread_params* params = (guacd_connection_io_thread_params*) data;
char buffer[8192];
@@ -132,6 +137,10 @@ static void* guacd_connection_write_thread(void* data) {
void* guacd_connection_io_thread(void* data) {
+ /* Thread name conn-read: forwards data from the connection's child
+ * process back to the connected user. */
+ guac_thread_name_set("conn-read");
+
guacd_connection_io_thread_params* params = (guacd_connection_io_thread_params*) data;
char buffer[8192];
@@ -372,6 +381,10 @@ static int guacd_route_connection(guacd_proc_map* map, guac_socket* socket) {
void* guacd_connection_thread(void* data) {
+ /* Thread name conn-route: performs the protocol handshake for a new
+ * client connection and routes it to a connection process. */
+ guac_thread_name_set("conn-route");
+
guacd_connection_thread_params* params = (guacd_connection_thread_params*) data;
guacd_proc_map* map = params->map;
@@ -409,4 +422,3 @@ void* guacd_connection_thread(void* data) {
return NULL;
}
-
diff --git a/src/guacd/daemon.c b/src/guacd/daemon.c
index b374eb9b..61b83e21 100644
--- a/src/guacd/daemon.c
+++ b/src/guacd/daemon.c
@@ -27,6 +27,7 @@
#include "proc-map.h"
#include <guacamole/mem.h>
+#include <guacamole/proctitle.h>
#ifdef ENABLE_SSL
#include <openssl/ssl.h>
@@ -296,6 +297,8 @@ static void stop_process_callback(guacd_proc* proc, void* data) {
int main(int argc, char* argv[]) {
+ guac_process_title_init(argc, argv);
+
/* Server */
int socket_fd;
struct addrinfo* addresses;
@@ -614,4 +617,3 @@ int main(int argc, char* argv[]) {
return 0;
}
-
diff --git a/src/guacd/proc.c b/src/guacd/proc.c
index 4040a3be..ec717d78 100644
--- a/src/guacd/proc.c
+++ b/src/guacd/proc.c
@@ -29,6 +29,7 @@
#include <guacamole/mem.h>
#include <guacamole/parser.h>
#include <guacamole/plugin.h>
+#include <guacamole/proctitle.h>
#include <guacamole/protocol.h>
#include <guacamole/socket.h>
#include <guacamole/user.h>
@@ -80,6 +81,10 @@ typedef struct guacd_user_thread_params {
*/
static void* guacd_user_thread(void* data) {
+ /* Thread name user-conn: manages a single user's connection lifecycle
+ * from handshake through disconnect. */
+ 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;
@@ -213,6 +218,10 @@ typedef struct guacd_client_free {
*/
static void* guacd_client_free_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");
+
guacd_client_free* free_operation = (guacd_client_free*) data;
/* Attempt to free client (this may never return if the client is
@@ -326,6 +335,11 @@ static 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. */
+ 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",
diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am
index 6e2e3f14..28984445 100644
--- a/src/libguac/Makefile.am
+++ b/src/libguac/Makefile.am
@@ -74,6 +74,7 @@ libguacinc_HEADERS = \
guacamole/plugin.h \
guacamole/pool.h \
guacamole/pool-types.h \
+ guacamole/proctitle.h \
guacamole/protocol.h \
guacamole/protocol-constants.h \
guacamole/protocol-types.h \
@@ -155,6 +156,7 @@ libguac_la_SOURCES = \
palette.c \
parser.c \
pool.c \
+ proctitle.c \
protocol.c \
raw_encoder.c \
recording.c \
@@ -209,4 +211,3 @@ libguac_la_LDFLAGS = \
@VORBIS_LIBS@ \
@WEBP_LIBS@ \
@WINSOCK_LIBS@
-
diff --git a/src/libguac/client.c b/src/libguac/client.c
index 5926e9ca..e9635c2a 100644
--- a/src/libguac/client.c
+++ b/src/libguac/client.c
@@ -28,6 +28,7 @@
#include "guacamole/layer.h"
#include "guacamole/plugin.h"
#include "guacamole/pool.h"
+#include "guacamole/proctitle.h"
#include "guacamole/protocol.h"
#include "guacamole/rwlock.h"
#include "guacamole/socket.h"
@@ -240,6 +241,10 @@ promotion_complete:
*/
static void* guac_client_pending_users_thread(void* data) {
+ /* Thread name user-pending: periodically promotes pending users into
+ * the active connection. */
+ guac_thread_name_set("user-pending");
+
guac_client* client = (guac_client*) data;
while (client->state == GUAC_CLIENT_RUNNING) {
diff --git a/src/libguac/display-render-thread.c b/src/libguac/display-render-thread.c
index 345ffb44..933442ad 100644
--- a/src/libguac/display-render-thread.c
+++ b/src/libguac/display-render-thread.c
@@ -23,6 +23,7 @@
#include "guacamole/display.h"
#include "guacamole/flag.h"
#include "guacamole/mem.h"
+#include "guacamole/proctitle.h"
#include "guacamole/timestamp.h"
/**
@@ -62,6 +63,10 @@
*/
static void* guac_display_render_loop(void* data) {
+ /* Thread name display-render: drives the display render loop, flushing
+ * completed frames to the client. */
+ guac_thread_name_set("display-render");
+
guac_display_render_thread* render_thread = (guac_display_render_thread*) data;
guac_display* display = render_thread->display;
guac_client* client = display->client;
diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c
index eb251263..7f500cf3 100644
--- a/src/libguac/display-worker.c
+++ b/src/libguac/display-worker.c
@@ -23,6 +23,7 @@
#include "guacamole/display.h"
#include "guacamole/fifo.h"
#include "guacamole/layer.h"
+#include "guacamole/proctitle.h"
#include "guacamole/protocol-types.h"
#include "guacamole/protocol.h"
#include "guacamole/rect.h"
@@ -293,6 +294,10 @@ static int LFR_guac_display_layer_should_use_webp(guac_display_layer* layer,
void* guac_display_worker_thread(void* data) {
+ /* Thread name display-wrk: one worker in the display pool; encodes and
+ * sends graphical updates for dirty layer regions. */
+ guac_thread_name_set("display-wrk");
+
int framerate;
int has_outstanding_frames = 0;
diff --git a/src/libguac/guacamole/proctitle.h b/src/libguac/guacamole/proctitle.h
new file mode 100644
index 00000000..30a7c0f0
--- /dev/null
+++ b/src/libguac/guacamole/proctitle.h
@@ -0,0 +1,167 @@
+/*
+ * 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 _GUAC_PROCTITLE_H
+#define _GUAC_PROCTITLE_H
+
+/**
+ * @file proctitle.h
+ *
+ * These functions allow guacd and its per-connection child processes to
+ * show meaningful process and thread names in tools such as `ps`, `top`,
+ * `htop`, and `gdb`. Process titles can be updated at runtime to identify
+ * the active connection (for example, `vnc user@example.com:5900`), while
+ * worker threads can be assigned short descriptive names (for example,
+ * `display-wrk` or `user-input`).
+ *
+ * Process titles are visible through normal process-listing interfaces and
+ * should be treated as local-observable metadata. Callers must not include
+ * passwords, tokens, keys, or other secrets.
+ *
+ * Process title support operates on a single argv/environ memory region
+ * captured once at process startup by guac_process_title_init(). The captured
+ * buffer address and size are stored as process-wide static state and
+ * reused by all subsequent title updates. Writes are serialized with a
+ * static mutex so callers can invoke from any thread.
+ *
+ * Linux exposes argv+environ as a contiguous block whose contents are
+ * returned through `/proc/<pid>/cmdline`. Process title updates work by:
+ * - Capturing that block's address and length at startup.
+ * - Moving `environ` to a heap copy so `getenv()` continues to work
+ * after the original storage is overwritten.
+ * - Reusing the captured block as a writable title buffer, NUL-padding
+ * any unused bytes.
+ *
+ * Thread names are exposed through Linux's thread `comm` mechanism:
+ * - `prctl(PR_SET_NAME)` updates the calling thread's name
+ * (15 characters plus NUL). Used by guac_thread_name_set().
+ *
+ * - Writing `/proc/self/task/<tid>/comm` updates the name of any
+ * thread in the process. guac_process_title_set() uses this to update
+ * the main thread so process-oriented tools such as `top` and
+ * `ps -e` display the active connection.
+ *
+ * --- When to call ---
+ *
+ * guac_process_title_init:
+ * Call once from main() before creating threads and before any
+ * code modifies `environ`.
+ *
+ * guac_process_title_set:
+ * Call any time after initialization. Typically used once per
+ * child process after the connection type and target are known.
+ *
+ * guac_thread_name_set:
+ * Call at thread startup to assign a descriptive worker name.
+ */
+
+/**
+ * Recommended buffer size for formatting a process title before passing it
+ * to guac_process_title_set(). Fits a protocol name, user, host, and port in
+ * typical deployments.
+ */
+#define GUAC_PROCESS_TITLE_BUFSIZE 256
+
+/**
+ * Initializes process title support by capturing the writable argv/environ
+ * region and moving `environ` to the heap. Must be called from main()
+ * before any other thread starts and before anything else reuses argv or
+ * environ. Safe to call more than once (subsequent calls no-op) and safe
+ * with bad args (no-op).
+ *
+ * If the heap environ copy fails to allocate, leaves environ in place and
+ * disables cmdline updates: later guac_process_title_set() calls still update
+ * the main-thread comm.
+ *
+ * @param argc
+ * The argument count as passed to main().
+ *
+ * @param argv
+ * The argument vector as passed to main().
+ */
+void guac_process_title_init(int argc, char** argv);
+
+/**
+ * Updates the process title: both /proc/<pid>/cmdline (the COMMAND column
+ * in `ps`/`htop`) and the *main thread's* short comm (visible in `top`,
+ * `ps -e`). The calling thread's own comm is left alone: use
+ * guac_thread_name_set() for that.
+ *
+ * If guac_process_title_init() was not called successfully, the cmdline part
+ * is skipped and only the comm write is attempted.
+ *
+ * @param title
+ * The new title. Truncated to fit the captured argv region (cmdline)
+ * and to 15 chars for the main thread comm. NULL no-ops.
+ */
+void guac_process_title_set(const char* title);
+
+/**
+ * Convenience wrapper around guac_process_title_set() that formats a network
+ * connection title in the form "<protocol> [user@]host[:port]". Falls back to
+ * "unknown-host" when host is NULL or empty, and omits the "user@" or ":port"
+ * portion when the respective argument is NULL or empty. The port is taken as
+ * a string so callers with either a numeric or named port can use it; numeric
+ * ports should be converted with guac_itoa_safe() first.
+ *
+ * The username is partially masked before display (e.g. "bbennett" becomes
+ * "bb****tt") so the full target account name is not exposed in world-readable
+ * process listings. Short or non-printable-ASCII usernames are masked in full.
+ * This is obfuscation to reduce casual disclosure, not an access control.
+ *
+ * @param protocol
+ * Short protocol label, e.g. "vnc", "rdp", "ssh". A NULL value no-ops.
+ *
+ * @param user
+ * The connecting username, or NULL/empty to omit the "user@" portion. The
+ * value is partially masked before it appears in the process title.
+ *
+ * @param host
+ * The target hostname, or NULL/empty to substitute "unknown-host".
+ *
+ * @param port
+ * The target port as a string, or NULL/empty to omit the ":port" portion.
+ */
+void guac_process_title_set_endpoint(const char* protocol, const char* user,
+ const char* host, const char* port);
+
+/**
+ * Sets the *calling* thread's short name (visible in
+ * /proc/<pid>/task/<tid>/comm, `top -H`, `ps -L`, ...)
+ * via prctl(PR_SET_NAME).
+ *
+ * Thread-safe: the underlying prctl targets only the calling thread.
+ *
+ * Note: threads spawned later by the calling thread inherit
+ * this name as their initial comm. If you call into a library
+ * that creates its own workers, those workers will appear with
+ * the parent's name until they rename themselves.
+ *
+ * Each call site is preceded by a comment of the form
+ * "Thread name <name>: <description>". To list every named thread
+ * and what it does, search the source. The description may wrap onto
+ * the following line, so include one line of trailing context:
+ *
+ * grep -rn -A1 "Thread name " src/
+ *
+ * @param name
+ * The new thread name. Truncated to 15 chars + NULL.
+ */
+void guac_thread_name_set(const char* name);
+
+#endif
diff --git a/src/libguac/proctitle.c b/src/libguac/proctitle.c
new file mode 100644
index 00000000..79ac17fe
--- /dev/null
+++ b/src/libguac/proctitle.c
@@ -0,0 +1,372 @@
+/*
+ * 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 "config.h"
+
+#include "guacamole/proctitle.h"
+
+#include <guacamole/mem.h>
+#include <guacamole/string.h>
+
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifdef HAVE_PRCTL
+#include <sys/prctl.h>
+#endif
+
+extern char** environ;
+
+/**
+ * The size, in bytes, of the buffer used to hold a thread "comm" name. The
+ * Linux kernel limits a thread's comm to TASK_COMM_LEN bytes (16), i.e. 15
+ * characters plus a null terminator, and silently truncates anything longer,
+ * so a larger buffer would serve no purpose.
+ */
+#define GUAC_PROCTITLE_COMM_LENGTH 16
+
+/**
+ * The size, in bytes, of the buffer used to hold the /proc path addressing a
+ * thread's comm pseudo-file. This must be large enough for
+ * GUAC_PROCTITLE_COMM_PATH expanded with the widest possible PID. A 64-bit
+ * PID is at most 20 decimal digits, and the surrounding literal text is 21
+ * characters, so 64 bytes leaves comfortable headroom.
+ */
+#define GUAC_PROCTITLE_COMM_PATH_LENGTH 64
+
+/**
+ * Format string for the /proc path of a thread's comm pseudo-file. The single
+ * argument is the target thread's TID; for the thread-group leader (the
+ * process "main" thread) this equals the process PID returned by getpid().
+ */
+#define GUAC_PROCTITLE_COMM_PATH "/proc/self/task/%ld/comm"
+
+/**
+ * The number of leading and trailing characters of a username preserved by
+ * mask_username(); the characters in between are replaced with a fixed run of
+ * asterisks.
+ */
+#define GUAC_PROCTITLE_USER_REVEAL 2
+
+/**
+ * The fixed asterisk run substituted for the masked (middle) portion of a
+ * username by mask_username(). Its width is intentionally constant, rather
+ * than matching the number of characters removed, so the obfuscated form does
+ * not leak the original username's length.
+ */
+#define GUAC_PROCTITLE_USER_MASK "****"
+
+/**
+ * The minimum username length, in bytes, for which mask_username() reveals the
+ * leading and trailing characters. Names shorter than twice
+ * GUAC_PROCTITLE_USER_REVEAL characters would have all (or overlapping)
+ * characters exposed, so they are masked in their entirety instead. The "+ 1"
+ * guarantees at least one character is always masked.
+ */
+#define GUAC_PROCTITLE_USER_MIN_REVEAL (2 * GUAC_PROCTITLE_USER_REVEAL + 1)
+
+/**
+ * The size, in bytes, of a buffer guaranteed to hold any string produced by
+ * mask_username(): GUAC_PROCTITLE_USER_REVEAL characters at each end plus the
+ * GUAC_PROCTITLE_USER_MASK run (whose sizeof() includes its NUL terminator).
+ */
+#define GUAC_PROCTITLE_USER_MASKED_BUFSIZE \
+ (2 * GUAC_PROCTITLE_USER_REVEAL + sizeof(GUAC_PROCTITLE_USER_MASK))
+
+/**
+ * The inclusive bounds of the printable ASCII range (space through tilde).
+ * mask_username() treats any username byte outside this range as
+ * non-printable and masks the username in full.
+ */
+#define ASCII_PRINTABLE_MIN 0x20
+#define ASCII_PRINTABLE_MAX 0x7E
+
+/**
+ * The start of the writable argv/environ area used for process titles.
+ */
+static char* guac_proctitle_buffer = NULL;
+
+/**
+ * The number of bytes available within guac_proctitle_buffer.
+ */
+static size_t guac_proctitle_buffer_size = 0;
+
+/**
+ * Serializes access to the process-global proctitle state: the argv overlay
+ * buffer and the /proc/<pid>/task/<tgid>/comm write. Static-initialized so
+ * the first caller does not race against a missing pthread_mutex_init().
+ */
+static pthread_mutex_t guac_proctitle_lock = PTHREAD_MUTEX_INITIALIZER;
+
+/**
+ * Updates the main thread's short comm name from any thread in the same
+ * process, without changing the calling thread's own comm. Writes through
+ * /proc/<pid>/task/<tid>/comm; the main thread is identified by
+ * TID == TGID == getpid(). This relies on the Linux procfs layout and is a
+ * no-op on platforms that lack it.
+ */
+static void guac_main_thread_name_set(const char* name) {
+
+#ifdef __linux__
+ char comm_path[GUAC_PROCTITLE_COMM_PATH_LENGTH];
+ char short_name[GUAC_PROCTITLE_COMM_LENGTH];
+ FILE* comm_file;
+
+ if (name == NULL)
+ return;
+
+ memset(short_name, '\0', sizeof(short_name));
+ guac_strlcpy(short_name, name, sizeof(short_name));
+
+ snprintf(comm_path, sizeof(comm_path), GUAC_PROCTITLE_COMM_PATH,
+ (long) getpid());
+
+ /* Write the new name to the main thread's comm pseudo-file. The kernel
+ * accepts a short string (no trailing newline required) and truncates
+ * to TASK_COMM_LEN. fopen() may fail in restricted environments where
+ * /proc is unavailable or write access is denied; treat that as a
+ * silent no-op since process naming is best-effort observability. */
+ comm_file = fopen(comm_path, "w");
+ if (comm_file == NULL)
+ return;
+
+ fputs(short_name, comm_file);
+ fclose(comm_file);
+#else
+ (void) name;
+#endif
+
+}
+
+void guac_process_title_init(int argc, char** argv) {
+
+ char* buffer_end;
+ char** copied_environ;
+ int envc = 0;
+ int i;
+
+ if (argc <= 0 || argv == NULL || argv[0] == NULL)
+ return;
+
+ pthread_mutex_lock(&guac_proctitle_lock);
+
+ /* Idempotent: a second init after the buffer is claimed is a no-op. */
+ if (guac_proctitle_buffer != NULL) {
+ pthread_mutex_unlock(&guac_proctitle_lock);
+ return;
+ }
+
+ buffer_end = argv[argc - 1] + strlen(argv[argc - 1]) + 1;
+
+ for (i = 0; environ != NULL && environ[i] != NULL; i++) {
+
+ if (buffer_end == environ[i])
+ buffer_end = environ[i] + strlen(environ[i]) + 1;
+
+ envc++;
+
+ }
+
+ /* Copy environ to heap so it survives later overwrites of the argv area.
+ * If any allocation fails, leave the original environ in place and bail
+ * out without committing partial state. */
+ copied_environ = guac_mem_alloc(sizeof(char*), envc + 1);
+ if (copied_environ == NULL) {
+ pthread_mutex_unlock(&guac_proctitle_lock);
+ return;
+ }
+
+ for (i = 0; i < envc; i++) {
+
+ copied_environ[i] = guac_strdup(environ[i]);
+ if (copied_environ[i] == NULL) {
+ while (i > 0)
+ guac_mem_free(copied_environ[--i]);
+ guac_mem_free(copied_environ);
+ pthread_mutex_unlock(&guac_proctitle_lock);
+ return;
+ }
+
+ }
+ copied_environ[envc] = NULL;
+
+ environ = copied_environ;
+ guac_proctitle_buffer = argv[0];
+ guac_proctitle_buffer_size = buffer_end - argv[0];
+
+ pthread_mutex_unlock(&guac_proctitle_lock);
+
+}
+
+void guac_process_title_set(const char* title) {
+
+ size_t title_length;
+
+ if (title == NULL)
+ return;
+
+ pthread_mutex_lock(&guac_proctitle_lock);
+
+ guac_main_thread_name_set(title);
+
+ if (guac_proctitle_buffer == NULL || guac_proctitle_buffer_size == 0) {
+ pthread_mutex_unlock(&guac_proctitle_lock);
+ return;
+ }
+
+ title_length = strlen(title);
+ if (title_length >= guac_proctitle_buffer_size)
+ title_length = guac_proctitle_buffer_size - 1;
+
+ memcpy(guac_proctitle_buffer, title, title_length);
+ guac_proctitle_buffer[title_length] = '\0';
+
+ if (title_length + 1 < guac_proctitle_buffer_size) {
+ memset(guac_proctitle_buffer + title_length + 1, '\0',
+ guac_proctitle_buffer_size - title_length - 1);
+ }
+
+ pthread_mutex_unlock(&guac_proctitle_lock);
+
+}
+
+/**
+ * Writes a partially obfuscated form of the given username into the provided
+ * buffer for inclusion in a process title. The goal is to retain enough of the
+ * username for an operator to recognize a session at a glance while keeping the
+ * full target account name out of world-readable process listings
+ * (/proc/<pid>/cmdline).
+ *
+ * The algorithm is:
+ *
+ * 1. A NULL or empty username yields an empty string, so the caller omits
+ * the "user@" portion of the title entirely.
+ *
+ * 2. A username containing any byte outside the printable ASCII range
+ * (0x20-0x7E) is masked in full (GUAC_PROCTITLE_USER_MASK). This avoids
+ * splitting a multi-byte UTF-8 codepoint and emitting garbage.
+ *
+ * 3. A username shorter than GUAC_PROCTITLE_USER_MIN_REVEAL bytes is masked
+ * in full, since revealing GUAC_PROCTITLE_USER_REVEAL characters at each
+ * end would otherwise expose every (or overlapping) character (e.g.
+ * "root").
+ *
+ * 4. Otherwise the first and last GUAC_PROCTITLE_USER_REVEAL characters are
+ * preserved and the middle is replaced with the fixed-width run
+ * GUAC_PROCTITLE_USER_MASK, e.g. "bbennett" -> "bb****tt". The run width
+ * is constant regardless of username length so the result does not leak
+ * the original length.
+ *
+ * @param user
+ * The username to obfuscate, which may be NULL.
+ *
+ * @param out
+ * The buffer to receive the NUL-terminated, obfuscated username. Should be
+ * at least GUAC_PROCTITLE_USER_MASKED_BUFSIZE bytes.
+ *
+ * @param out_size
+ * The size of the out buffer, in bytes.
+ */
+static void mask_username(const char* user, char* out, size_t out_size) {
+
+ if (user == NULL || *user == '\0') {
+ out[0] = '\0';
+ return;
+ }
+
+ size_t length = strlen(user);
+
+ /* Reveal the prefix & suffix edges only for sufficiently long, plain
+ * printable-ASCII usernames; otherwise mask the entire value. */
+ int reveal = (length >= GUAC_PROCTITLE_USER_MIN_REVEAL);
+ for (size_t i = 0; reveal && i < length; i++) {
+ unsigned char c = (unsigned char) user[i];
+ if (c < ASCII_PRINTABLE_MIN || c > ASCII_PRINTABLE_MAX)
+ reveal = 0;
+ }
+
+ if (!reveal) {
+ guac_strlcpy(out, GUAC_PROCTITLE_USER_MASK, out_size);
+ return;
+ }
+
+ /* Preserve the first and last GUAC_PROCTITLE_USER_REVEAL characters,
+ * replacing everything between them with the fixed mask. */
+ char prefix[GUAC_PROCTITLE_USER_REVEAL + 1];
+ memcpy(prefix, user, GUAC_PROCTITLE_USER_REVEAL);
+ prefix[GUAC_PROCTITLE_USER_REVEAL] = '\0';
+
+ const char* suffix = user + length - GUAC_PROCTITLE_USER_REVEAL;
+
+ snprintf(out, out_size, "%s%s%s", prefix, GUAC_PROCTITLE_USER_MASK, suffix);
+
+}
+
+void guac_process_title_set_endpoint(const char* protocol, const char* user,
+ const char* host, const char* port) {
+
+ char title[GUAC_PROCESS_TITLE_BUFSIZE];
+ char masked_user[GUAC_PROCTITLE_USER_MASKED_BUFSIZE];
+
+ if (protocol == NULL)
+ return;
+
+ /* Fall back to a placeholder host; omit the user and port portions when
+ * not provided. */
+ if (host == NULL || *host == '\0')
+ host = "unknown-host";
+
+ /* The username is partially masked before display: it is target account
+ * metadata exposed in world-readable process listings. */
+ mask_username(user, masked_user, sizeof(masked_user));
+
+ int has_user = (masked_user[0] != '\0');
+ int has_port = (port != NULL && *port != '\0');
+
+ if (has_user && has_port)
+ snprintf(title, sizeof(title), "%s %s@%s:%s", protocol, masked_user, host, port);
+ else if (has_user)
+ snprintf(title, sizeof(title), "%s %s@%s", protocol, masked_user, host);
+ else if (has_port)
+ snprintf(title, sizeof(title), "%s %s:%s", protocol, host, port);
+ else
+ snprintf(title, sizeof(title), "%s %s", protocol, host);
+
+ guac_process_title_set(title);
+
+}
+
+void guac_thread_name_set(const char* name) {
+
+#ifdef HAVE_PRCTL
+ char short_name[GUAC_PROCTITLE_COMM_LENGTH];
+
+ if (name == NULL)
+ return;
+
+ /* guac_strlcpy() properly terminates short_name[]. */
+ guac_strlcpy(short_name, name, sizeof(short_name));
+ prctl(PR_SET_NAME, short_name, 0, 0, 0);
+#else
+ (void) name;
+#endif
+
+}
diff --git a/src/libguac/socket.c b/src/libguac/socket.c
index 2c2cd874..4dfc2567 100644
--- a/src/libguac/socket.c
+++ b/src/libguac/socket.c
@@ -20,6 +20,7 @@
#include "config.h"
#include "guacamole/mem.h"
#include "guacamole/error.h"
+#include "guacamole/proctitle.h"
#include "guacamole/protocol.h"
#include "guacamole/socket.h"
#include "guacamole/timestamp.h"
@@ -45,6 +46,10 @@ char __guac_socket_BASE64_CHARACTERS[64] = {
static void* __guac_socket_keep_alive_thread(void* data) {
+ /* Thread name keep-alive: periodically sends keep-alive NOPs on an
+ * otherwise idle socket. */
+ guac_thread_name_set("keep-alive");
+
int old_cancelstate;
/* Socket keep-alive loop */
diff --git a/src/libguac/tcp.c b/src/libguac/tcp.c
index ba797658..b4b12ba6 100644
--- a/src/libguac/tcp.c
+++ b/src/libguac/tcp.c
@@ -30,6 +30,11 @@
#include <sys/socket.h>
#include <unistd.h>
+/* Fallback for platforms that do not define EBADFD. */
+#ifndef EBADFD
+#define EBADFD EBADF
+#endif
+
int guac_tcp_connect(const char* hostname, const char* port, const int timeout) {
int retval;
diff --git a/src/libguac/user-handshake.c b/src/libguac/user-handshake.c
index 422c941b..f0aa35ce 100644
--- a/src/libguac/user-handshake.c
+++ b/src/libguac/user-handshake.c
@@ -23,6 +23,7 @@
#include "guacamole/client.h"
#include "guacamole/error.h"
#include "guacamole/parser.h"
+#include "guacamole/proctitle.h"
#include "guacamole/protocol.h"
#include "guacamole/socket.h"
#include "guacamole/user.h"
@@ -129,6 +130,10 @@ static void guac_user_log_handshake_failure(guac_user* user) {
*/
static void* guac_user_input_thread(void* data) {
+ /* Thread name user-input: reads and parses instructions from a single
+ * user's socket. */
+ guac_thread_name_set("user-input");
+
guac_user_input_thread_params* params =
(guac_user_input_thread_params*) data;
diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c
index e32925d2..af2005f7 100644
--- a/src/protocols/kubernetes/kubernetes.c
+++ b/src/protocols/kubernetes/kubernetes.c
@@ -29,6 +29,7 @@
#include <guacamole/client.h>
#include <guacamole/mem.h>
+#include <guacamole/proctitle.h>
#include <guacamole/protocol.h>
#include <guacamole/recording.h>
#include <libwebsockets.h>
@@ -176,6 +177,10 @@ struct lws_protocols guac_kubernetes_lws_protocols[] = {
*/
static void* guac_kubernetes_input_thread(void* data) {
+ /* Thread name k8s-input: reads user input and forwards it to the
+ * Kubernetes pod. */
+ guac_thread_name_set("k8s-input");
+
guac_client* client = (guac_client*) data;
guac_kubernetes_client* kubernetes_client =
(guac_kubernetes_client*) client->data;
@@ -198,6 +203,10 @@ static void* guac_kubernetes_input_thread(void* data) {
void* guac_kubernetes_client_thread(void* data) {
+ /* Thread name k8s-worker: main Kubernetes client thread; manages the
+ * websocket connection to the pod. */
+ guac_thread_name_set("k8s-worker");
+
guac_client* client = (guac_client*) data;
guac_kubernetes_client* kubernetes_client =
(guac_kubernetes_client*) client->data;
@@ -214,6 +223,30 @@ void* guac_kubernetes_client_thread(void* data) {
goto fail;
}
+ const char* kubernetes_namespace = settings->kubernetes_namespace;
+ char kubernetes_title[GUAC_PROCESS_TITLE_BUFSIZE];
+
+ /* Namespace should already be populated by argument parsing, but
+ * provide fallback. */
+ if (kubernetes_namespace == NULL || *kubernetes_namespace == '\0')
+ kubernetes_namespace = GUAC_KUBERNETES_DEFAULT_NAMESPACE;
+
+ /* Identify the attached Kubernetes target (for example,
+ * "k8s default/mypod" or "k8s default/mypod/container"). Include the
+ * container when specified to distinguish multi-container pods. */
+ if (settings->kubernetes_container != NULL
+ && *settings->kubernetes_container != '\0')
+ snprintf(kubernetes_title, sizeof(kubernetes_title),
+ "%s %s/%s/%s", GUAC_KUBERNETES_PROCESS_TITLE_NAME,
+ kubernetes_namespace, settings->kubernetes_pod,
+ settings->kubernetes_container);
+ else
+ snprintf(kubernetes_title, sizeof(kubernetes_title),
+ "%s %s/%s", GUAC_KUBERNETES_PROCESS_TITLE_NAME,
+ kubernetes_namespace, settings->kubernetes_pod);
+
+ guac_process_title_set(kubernetes_title);
+
/* Generate endpoint for attachment URL */
if (guac_kubernetes_endpoint_uri(endpoint_path, sizeof(endpoint_path),
settings->kubernetes_namespace,
diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h
index 193f870c..50e80612 100644
--- a/src/protocols/kubernetes/settings.h
+++ b/src/protocols/kubernetes/settings.h
@@ -30,6 +30,12 @@
*/
#define GUAC_KUBERNETES_DEFAULT_PORT 8080
+/**
+ * The protocol label included in the process title (the first argument passed
+ * to guac_process_title_set_endpoint()), as seen in `ps`/`top`.
+ */
+#define GUAC_KUBERNETES_PROCESS_TITLE_NAME "k8s"
+
/**
* The name of the Kubernetes namespace that should be used by default if no
* specific Kubernetes namespace is provided.
diff --git a/src/protocols/rdp/channels/audio-input/audio-buffer.c b/src/protocols/rdp/channels/audio-input/audio-buffer.c
index 38d5b7f1..24b3d05f 100644
--- a/src/protocols/rdp/channels/audio-input/audio-buffer.c
+++ b/src/protocols/rdp/channels/audio-input/audio-buffer.c
@@ -22,6 +22,7 @@
#include <guacamole/client.h>
#include <guacamole/mem.h>
+#include <guacamole/proctitle.h>
#include <guacamole/protocol.h>
#include <guacamole/socket.h>
#include <guacamole/stream.h>
@@ -217,6 +218,10 @@ static void guac_rdp_audio_buffer_wait(guac_rdp_audio_buffer* audio_buffer) {
*/
static void* guac_rdp_audio_buffer_flush_thread(void* data) {
+ /* Thread name rdp-audio: flushes buffered audio input to the RDP
+ * server at the negotiated rate. */
+ guac_thread_name_set("rdp-audio");
+
guac_rdp_audio_buffer* audio_buffer = (guac_rdp_audio_buffer*) data;
while (!audio_buffer->stopping) {
diff --git a/src/protocols/rdp/print-job.c b/src/protocols/rdp/print-job.c
index eda0275a..d899598b 100644
--- a/src/protocols/rdp/print-job.c
+++ b/src/protocols/rdp/print-job.c
@@ -23,6 +23,7 @@
#include <guacamole/client.h>
#include <guacamole/error.h>
#include <guacamole/mem.h>
+#include <guacamole/proctitle.h>
#include <guacamole/protocol.h>
#include <guacamole/socket.h>
#include <guacamole/stream.h>
@@ -392,6 +393,10 @@ static pid_t guac_rdp_create_filter_process(guac_client* client,
*/
static void* guac_rdp_print_job_output_thread(void* data) {
+ /* Thread name rdp-print: streams output from the RDP print filter
+ * process to the client as a downloadable file. */
+ guac_thread_name_set("rdp-print");
+
int length;
char buffer[6048];
diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c
index 3342079d..23ed9bc4 100644
--- a/src/protocols/rdp/rdp.c
+++ b/src/protocols/rdp/rdp.c
@@ -57,6 +57,7 @@
#include <freerdp/freerdp.h>
#include <freerdp/gdi/gdi.h>
#include <freerdp/graphics.h>
+#include <guacamole/proctitle.h>
#include <freerdp/primary.h>
#include <freerdp/settings.h>
#include <freerdp/update.h>
@@ -732,10 +733,20 @@ fail:
void* guac_rdp_client_thread(void* data) {
+ /* Thread name rdp-worker: main RDP client thread; runs the FreeRDP
+ * connection and event loop. */
+ guac_thread_name_set("rdp-worker");
+
guac_client* client = (guac_client*) data;
guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
guac_rdp_settings* settings = rdp_client->settings;
+ char rdp_port[GUAC_USHORT_STRING_BUFSIZE];
+ if (guac_itoa_safe(rdp_port, sizeof(rdp_port), settings->port) < 1)
+ rdp_port[0] = '\0';
+ guac_process_title_set_endpoint(GUAC_RDP_PROCESS_TITLE_NAME,
+ settings->username, settings->hostname, rdp_port);
+
/* If Wake-on-LAN is enabled, attempt to wake. */
if (settings->wol_send_packet) {
diff --git a/src/protocols/rdp/settings.h b/src/protocols/rdp/settings.h
index f23cefbe..8ce5f030 100644
--- a/src/protocols/rdp/settings.h
+++ b/src/protocols/rdp/settings.h
@@ -42,6 +42,12 @@
*/
#define RDP_DEFAULT_PORT 3389
+/**
+ * The protocol label included in the process title (the first argument passed
+ * to guac_process_title_set_endpoint()), as seen in `ps`/`top`.
+ */
+#define GUAC_RDP_PROCESS_TITLE_NAME "rdp"
+
/**
* The default SFTP connection timeout, in seconds.
*/
diff --git a/src/protocols/ssh/settings.h b/src/protocols/ssh/settings.h
index 606177c5..e3633f12 100644
--- a/src/protocols/ssh/settings.h
+++ b/src/protocols/ssh/settings.h
@@ -30,6 +30,12 @@
*/
#define GUAC_SSH_DEFAULT_PORT "22"
+/**
+ * The protocol label included in the process title (the first argument passed
+ * to guac_process_title_set_endpoint()), as seen in `ps`/`top`.
+ */
+#define GUAC_SSH_PROCESS_TITLE_NAME "ssh"
+
/**
* The default number of seconds to attempt a connection to the SSH/SFTP
* server before giving up.
diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c
index 508492b7..cd91bd9f 100644
--- a/src/protocols/ssh/ssh.c
+++ b/src/protocols/ssh/ssh.c
@@ -37,6 +37,7 @@
#include <guacamole/client.h>
#include <guacamole/error.h>
#include <guacamole/mem.h>
+#include <guacamole/proctitle.h>
#include <guacamole/recording.h>
#include <guacamole/socket.h>
#include <guacamole/timestamp.h>
@@ -201,6 +202,10 @@ static char* guac_ssh_get_credential(guac_client *client, char* cred_name) {
void* ssh_input_thread(void* data) {
+ /* Thread name ssh-stdin: reads terminal STDIN and forwards it to the
+ * SSH server. */
+ guac_thread_name_set("ssh-stdin");
+
guac_client* client = (guac_client*) data;
guac_ssh_client* ssh_client = (guac_ssh_client*) client->data;
@@ -226,6 +231,10 @@ void* ssh_input_thread(void* data) {
void* ssh_client_thread(void* data) {
+ /* Thread name ssh-worker: main SSH client thread; runs the SSH session
+ * and drives the terminal. */
+ guac_thread_name_set("ssh-worker");
+
guac_client* client = (guac_client*) data;
guac_ssh_client* ssh_client = (guac_ssh_client*) client->data;
guac_ssh_settings* settings = ssh_client->settings;
@@ -343,6 +352,11 @@ void* ssh_client_thread(void* data) {
/* Ensure connection is kept alive during lengthy connects */
guac_socket_require_keep_alive(client->socket);
+ const char* ssh_port = settings->port != NULL
+ ? settings->port : GUAC_SSH_DEFAULT_PORT;
+ guac_process_title_set_endpoint(GUAC_SSH_PROCESS_TITLE_NAME,
+ settings->username, settings->hostname, ssh_port);
+
/* Open SSH session */
ssh_client->session = guac_common_ssh_create_session(client,
settings->hostname, settings->port, ssh_client->user,
diff --git a/src/protocols/telnet/settings.h b/src/protocols/telnet/settings.h
index 43432619..f341fc6a 100644
--- a/src/protocols/telnet/settings.h
+++ b/src/protocols/telnet/settings.h
@@ -31,6 +31,12 @@
*/
#define GUAC_TELNET_DEFAULT_PORT "23"
+/**
+ * The protocol label included in the process title (the first argument passed
+ * to guac_process_title_set_endpoint()), as seen in `ps`/`top`.
+ */
+#define GUAC_TELNET_PROCESS_TITLE_NAME "telnet"
+
/**
* The default number of seconds to wait for a successful connection before
* timing out.
diff --git a/src/protocols/telnet/telnet.c b/src/protocols/telnet/telnet.c
index 71635e6e..948cdc4a 100644
--- a/src/protocols/telnet/telnet.c
+++ b/src/protocols/telnet/telnet.c
@@ -26,6 +26,7 @@
#include <guacamole/client.h>
#include <guacamole/error.h>
#include <guacamole/mem.h>
+#include <guacamole/proctitle.h>
#include <guacamole/protocol.h>
#include <guacamole/recording.h>
#include <guacamole/tcp.h>
@@ -358,6 +359,10 @@ static void __guac_telnet_event_handler(telnet_t* telnet, telnet_event_t* event,
*/
static void* __guac_telnet_input_thread(void* data) {
+ /* Thread name telnet-stdin: reads terminal STDIN and forwards it to the
+ * telnet server. */
+ guac_thread_name_set("telnet-stdin");
+
guac_client* client = (guac_client*) data;
guac_telnet_client* telnet_client = (guac_telnet_client*) client->data;
@@ -491,6 +496,10 @@ static int __guac_telnet_wait(int socket_fd) {
void* guac_telnet_client_thread(void* data) {
+ /* Thread name telnet-worker: main telnet client thread; runs the
+ * telnet session and event loop. */
+ guac_thread_name_set("telnet-worker");
+
guac_client* client = (guac_client*) data;
guac_telnet_client* telnet_client = (guac_telnet_client*) client->data;
guac_telnet_settings* settings = telnet_client->settings;
@@ -499,6 +508,11 @@ void* guac_telnet_client_thread(void* data) {
char buffer[8192];
int wait_result;
+ const char* telnet_port = settings->port != NULL
+ ? settings->port : GUAC_TELNET_DEFAULT_PORT;
+ guac_process_title_set_endpoint(GUAC_TELNET_PROCESS_TITLE_NAME,
+ settings->username, settings->hostname, telnet_port);
+
/* If Wake-on-LAN is enabled, attempt to wake. */
if (settings->wol_send_packet) {
diff --git a/src/protocols/vnc/settings.h b/src/protocols/vnc/settings.h
index f63cf807..ac914f66 100644
--- a/src/protocols/vnc/settings.h
+++ b/src/protocols/vnc/settings.h
@@ -22,6 +22,12 @@
#include <stdbool.h>
+/**
+ * The protocol label included in the process title (the first argument passed
+ * to guac_process_title_set_endpoint()), as seen in `ps`/`top`.
+ */
+#define GUAC_VNC_PROCESS_TITLE_NAME "vnc"
+
/**
* The filename to use for the screen recording, if not specified.
*/
diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c
index 1d8ab0c7..3a1c2479 100644
--- a/src/protocols/vnc/vnc.c
+++ b/src/protocols/vnc/vnc.c
@@ -42,6 +42,7 @@
#include <guacamole/client.h>
#include <guacamole/display.h>
#include <guacamole/mem.h>
+#include <guacamole/proctitle.h>
#include <guacamole/protocol.h>
#include <guacamole/recording.h>
#include <guacamole/socket.h>
@@ -452,10 +453,24 @@ static rfbBool guac_vnc_handle_messages(guac_client* client) {
void* guac_vnc_client_thread(void* data) {
+ /* Thread name vnc-worker: main VNC client thread; runs the libvncclient
+ * connection and message loop. */
+ guac_thread_name_set("vnc-worker");
+
guac_client* client = (guac_client*) data;
guac_vnc_client* vnc_client = (guac_vnc_client*) client->data;
guac_vnc_settings* settings = vnc_client->settings;
+ /* VNC has no default port (0 == unspecified), so suppress a misleading
+ * ":0" in the title. */
+ char vnc_port[GUAC_USHORT_STRING_BUFSIZE];
+ if (settings->port == 0
+ || guac_itoa_safe(vnc_port, sizeof(vnc_port),
+ settings->port) < 1)
+ vnc_port[0] = '\0';
+ guac_process_title_set_endpoint(GUAC_VNC_PROCESS_TITLE_NAME,
+ settings->username, settings->hostname, vnc_port);
+
/* If Wake-on-LAN is enabled, attempt to wake. */
if (settings->wol_send_packet) {
diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c
index d6b4004c..a8ab2306 100644
--- a/src/terminal/terminal.c
+++ b/src/terminal/terminal.c
@@ -49,6 +49,7 @@
#include <guacamole/error.h>
#include <guacamole/flag.h>
#include <guacamole/mem.h>
+#include <guacamole/proctitle.h>
#include <guacamole/protocol.h>
#include <guacamole/socket.h>
#include <guacamole/string.h>
@@ -352,6 +353,10 @@ static void guac_terminal_repaint_default_layer(guac_terminal* terminal,
*/
void* guac_terminal_thread(void* data) {
+ /* Thread name terminal: renders the terminal emulator display and
+ * processes output from the remote. */
+ guac_thread_name_set("term-render");
+
guac_terminal* terminal = (guac_terminal*) data;
guac_client* client = terminal->client;