blob: 990527e9552524ca621775c59ded78ccf285c89e [file]
/*
* 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 "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
}