VYPR
Medium severity6.3NVD Advisory· Published May 27, 2026

CVE-2026-47270

CVE-2026-47270

Description

pam_usb provides hardware authentication for Linux using ordinary removable media. Prior to 0.9.0, pam_usb is a PAM module loaded into the host process (sudo, login, GDM, GNOME Shell). Display managers such as GDM run multiple concurrent authentication threads. Three functions used by the deny_remote feature called the non-reentrant strtok(), which stores state in a single global pointer. If two authentications race, one thread's strtok() call can overwrite the other's in-progress tokenisation pointer, causing incorrect parsing of the tmux session data or the /proc environ scan that backs the remote-session detection logic. Additionally, pusb_tmux_get_client_tty() passed the raw pointer returned by getenv(TMUX) directly to strtok(). getenv() returns a pointer into the live process environment block; strtok() inserts NUL bytes into that block, permanently corrupting the TMUX variable for subsequent code running in the same process. In long-lived display managers this affects all future authentications in that process. The combined effect can cause deny_remote=true to return an incorrect decision for a remote session, or an incorrect decision for a local session, depending on thread interleaving. This vulnerability is fixed in 0.9.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

pam_usb 0.8.x uses non-reentrant strtok() in deny_remote, causing race conditions and environment corruption that can lead to incorrect authentication decisions.

Vulnerability

pam_usb versions prior to 0.9.0 contain a race condition and environment corruption vulnerability in the deny_remote feature. Three functions — pusb_tmux_get_client_tty(), pusb_tmux_has_remote_clients(), and pusb_get_process_envvar() — use the non-reentrant strtok() function, which stores state in a single global pointer. Additionally, pusb_tmux_get_client_tty() passes the raw pointer returned by getenv("TMUX") directly to strtok(), which inserts NUL bytes into the live process environment block, permanently corrupting the TMUX variable for all subsequent code in the same process [1][3]. This affects display managers like GDM that run multiple concurrent authentication threads.

Exploitation

An attacker can exploit this vulnerability by triggering concurrent authentication attempts in a multi-threaded PAM host (e.g., GDM, GNOME Shell). The race condition allows one thread's strtok() call to overwrite another thread's in-progress tokenisation pointer, causing incorrect parsing of tmux session data or /proc environ scans used by deny_remote detection logic. The environment corruption persists across authentications in long-lived processes. No special privileges are required beyond the ability to initiate authentication requests; the attacker may be a remote user attempting to authenticate while a local authentication is in progress [3].

Impact

Successful exploitation can cause the deny_remote=true setting to return an incorrect decision: either allowing a remote session when it should be denied, or denying a local session. This can lead to unauthorized access to systems that rely on pam_usb for hardware authentication, potentially compromising the authentication mechanism for sudo, login, or display manager sessions [3].

Mitigation

The vulnerability is fixed in pam_usb version 0.9.0. The fix replaces all strtok() calls with the reentrant strtok_r() using per-call saveptr locals, and pusb_tmux_get_client_tty() now operates on a private copy of the TMUX value via xstrdup() [1][2]. No workarounds are documented; users should upgrade to 0.9.0 or later. The CVE is not listed in CISA's Known Exploited Vulnerabilities catalog as of publication.

AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Mcdope/Pam Usbreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: <0.9.0

Patches

2
94f1640a61d4

Fix deferred findings F6 (TOCTOU) and F10 (snprintf truncation) in pad.c

https://github.com/mcdope/pam_usbMcDopeMay 17, 2026via nvd-ref
2 files changed · +107 6
  • src/pad.c+48 6 modified
    @@ -46,7 +46,12 @@ static int pusb_pad_build_device_path(
     	char path_devpad[1024*5];
     	struct stat sb;
     
    -	snprintf(path_devpad, sizeof(path_devpad), "%s/%s", mnt_point, opts->device_pad_directory);
    +	int pn1 = snprintf(path_devpad, sizeof(path_devpad), "%s/%s", mnt_point, opts->device_pad_directory);
    +	if (pn1 < 0 || (size_t)pn1 >= sizeof(path_devpad))
    +	{
    +		log_error("Device pad directory path too long.\n");
    +		return 0;
    +	}
     	if (lstat(path_devpad, &sb) != 0)
     	{
     		log_debug("Directory %s does not exist, creating it.\n", path_devpad);
    @@ -62,7 +67,7 @@ static int pusb_pad_build_device_path(
     		return 0;
     	}
     
    -	snprintf(
    +	int pn2 = snprintf(
     		path_out,
     		path_size,
     		"%s/%s/%s.%s.pad",
    @@ -71,6 +76,11 @@ static int pusb_pad_build_device_path(
     		user,
     		opts->hostname
     	);
    +	if (pn2 < 0 || (size_t)pn2 >= path_size)
    +	{
    +		log_error("Device pad file path too long.\n");
    +		return 0;
    +	}
     	return 1;
     }
     
    @@ -93,13 +103,18 @@ static int pusb_pad_build_system_path(
     		return 0;
     	}
     
    -	snprintf(
    +	int sn1 = snprintf(
     		dir_path,
     		sizeof(dir_path),
     		"%s/%s",
     		user_ent->pw_dir,
     		opts->system_pad_directory
     	);
    +	if (sn1 < 0 || (size_t)sn1 >= sizeof(dir_path))
    +	{
    +		log_error("System pad directory path too long.\n");
    +		return 0;
    +	}
     	if (lstat(dir_path, &sb) != 0)
     	{
     		log_debug("Directory %s does not exist, creating one.\n", dir_path);
    @@ -133,17 +148,44 @@ static int pusb_pad_build_system_path(
     		device_name_ptr++;
     	}
     
    -	snprintf(
    +	int sn2 = snprintf(
     		path_out,
     		path_size,
     		"%s/%s/%s.pad",
     		user_ent->pw_dir,
     		opts->system_pad_directory,
     		device_name
     	);
    +	if (sn2 < 0 || (size_t)sn2 >= path_size)
    +	{
    +		log_error("System pad file path too long.\n");
    +		return 0;
    +	}
     	return 1;
     }
     
    +static int open_pad_file_in_dir(const char *fullpath, int flags)
    +{
    +	char dirbuf[1024 * 5];
    +	int n = snprintf(dirbuf, sizeof(dirbuf), "%s", fullpath);
    +	if (n < 0 || (size_t)n >= sizeof(dirbuf))
    +		return -1;
    +
    +	char *sep = strrchr(dirbuf, '/');
    +	if (sep == NULL || sep == dirbuf)
    +		return -1;
    +	*sep = '\0';
    +	const char *filename = sep + 1;
    +
    +	int dirfd = open(dirbuf, O_DIRECTORY | O_NOFOLLOW | O_RDONLY | O_CLOEXEC);
    +	if (dirfd == -1)
    +		return -1;
    +
    +	int fd = openat(dirfd, filename, flags | O_NOFOLLOW | O_CLOEXEC, 0600);
    +	close(dirfd);
    +	return fd;
    +}
    +
     static FILE *pusb_pad_open_device(
     	t_pusb_options *opts,
     	const char *mnt_point,
    @@ -158,7 +200,7 @@ static FILE *pusb_pad_open_device(
     	{
     		return NULL;
     	}
    -	int fd = open(path, flags | O_NOFOLLOW | O_CLOEXEC, 0600);
    +	int fd = open_pad_file_in_dir(path, flags);
     	if (fd < 0)
     	{
     		log_debug("Cannot open device file: %s\n", strerror(errno));
    @@ -187,7 +229,7 @@ static FILE *pusb_pad_open_system(
     	{
     		return NULL;
     	}
    -	int fd = open(path, flags | O_NOFOLLOW | O_CLOEXEC, 0600);
    +	int fd = open_pad_file_in_dir(path, flags);
     	if (fd < 0)
     	{
     		log_debug("Cannot open system file: %s\n", strerror(errno));
    
  • tests/unit/c/pad_test.c+59 0 modified
    @@ -394,6 +394,63 @@ static void test_generate_random_bytes_fills_buffer(void **state)
     	assert_int_not_equal(0, (int)acc);
     }
     
    +/* ── F6 regression: open_pad_file_in_dir rejects symlink at parent directory ── */
    +
    +static void test_open_pad_file_in_dir_rejects_dir_symlink(void **state)
    +{
    +	(void)state;
    +
    +	/* A real directory containing a real file */
    +	char real_dir[] = "/tmp/pamusb_f6real_XXXXXX";
    +	assert_non_null(mkdtemp(real_dir));
    +	char real_file[512];
    +	snprintf(real_file, sizeof(real_file), "%s/test.pad", real_dir);
    +	write_file(real_file, "x", 1);
    +
    +	/* A temp dir containing a symlink that points at real_dir */
    +	char base_dir[] = "/tmp/pamusb_f6base_XXXXXX";
    +	assert_non_null(mkdtemp(base_dir));
    +	char sym_dir[512];
    +	snprintf(sym_dir, sizeof(sym_dir), "%s/pads", base_dir);
    +	assert_int_equal(0, symlink(real_dir, sym_dir));
    +
    +	/* Path whose parent component is a symlink */
    +	char path[1024];
    +	snprintf(path, sizeof(path), "%s/test.pad", sym_dir);
    +
    +	/* open_pad_file_in_dir must fail: O_NOFOLLOW rejects symlink as directory */
    +	int fd = open_pad_file_in_dir(path, O_RDONLY);
    +	assert_true(fd < 0);
    +	if (fd >= 0) close(fd);
    +
    +	unlink(sym_dir);
    +	unlink(real_file);
    +	rmdir(real_dir);
    +	rmdir(base_dir);
    +}
    +
    +/* ── F10 regression: device pad path truncation denied ── */
    +
    +static void test_device_pad_path_too_long_denied(void **state)
    +{
    +	(void)state;
    +
    +	t_pusb_options opts = {0};
    +	pusb_conf_init(&opts);
    +	/* Fill device_pad_directory to near PATH_MAX (4095 'a' chars) */
    +	memset(opts.device_pad_directory, 'a', sizeof(opts.device_pad_directory) - 1);
    +	opts.device_pad_directory[sizeof(opts.device_pad_directory) - 1] = '\0';
    +
    +	/* mnt_point of 1100 chars: 1100 + 1 + 4095 = 5196 > 5120 -- triggers truncation check */
    +	char long_mnt[1101];
    +	memset(long_mnt, 'b', sizeof(long_mnt) - 1);
    +	long_mnt[sizeof(long_mnt) - 1] = '\0';
    +
    +	char path_out[1024 * 5];
    +	int result = pusb_pad_build_device_path(&opts, long_mnt, "user", path_out, sizeof(path_out));
    +	assert_int_equal(0, result);
    +}
    +
     /* ── F4 regression: timingsafe_memcmp correctness ── */
     
     static void test_timingsafe_memcmp_equal(void **state)
    @@ -434,6 +491,8 @@ int main(void)
     		cmocka_unit_test(test_pad_missing),
     		cmocka_unit_test(test_first_run_no_pads_allowed),
     		cmocka_unit_test(test_missing_system_pad_denied),
    +		cmocka_unit_test(test_open_pad_file_in_dir_rejects_dir_symlink),
    +		cmocka_unit_test(test_device_pad_path_too_long_denied),
     		cmocka_unit_test(test_generate_random_bytes_fills_buffer),
     		cmocka_unit_test(test_timingsafe_memcmp_equal),
     		cmocka_unit_test(test_timingsafe_memcmp_differ),
    
d003e551b794

Security audit round 3: fix 8 findings across 6 source files

https://github.com/mcdope/pam_usbMcDopeMay 17, 2026via nvd-ref
8 files changed · +134 32
  • src/device.c+6 5 modified
    @@ -15,6 +15,7 @@
      * Street, Fifth Floor, Boston, MA 02110-1301 USA
      */
     
    +#include <stdio.h>
     #include <unistd.h>
     #include <stdlib.h>
     #include <string.h>
    @@ -76,11 +77,11 @@ static int pusb_device_connected(t_pusb_options *opts, UDisksClient *udisks)
     
     				g_object_unref(drive);
     				if (retval) {
    -					strncpy(opts->device.name, opts->device_list[currentDevice].name, sizeof(opts->device.name) - 1);
    -					strncpy(opts->device.vendor, opts->device_list[currentDevice].vendor, sizeof(opts->device.vendor) - 1);
    -					strncpy(opts->device.model, opts->device_list[currentDevice].model, sizeof(opts->device.model) - 1);
    -					strncpy(opts->device.serial, opts->device_list[currentDevice].serial, sizeof(opts->device.serial) - 1);
    -					strncpy(opts->device.volume_uuid, opts->device_list[currentDevice].volume_uuid, sizeof(opts->device.volume_uuid) - 1);
    +					snprintf(opts->device.name,        sizeof(opts->device.name),        "%s", opts->device_list[currentDevice].name);
    +					snprintf(opts->device.vendor,      sizeof(opts->device.vendor),      "%s", opts->device_list[currentDevice].vendor);
    +					snprintf(opts->device.model,       sizeof(opts->device.model),       "%s", opts->device_list[currentDevice].model);
    +					snprintf(opts->device.serial,      sizeof(opts->device.serial),      "%s", opts->device_list[currentDevice].serial);
    +					snprintf(opts->device.volume_uuid, sizeof(opts->device.volume_uuid), "%s", opts->device_list[currentDevice].volume_uuid);
     					currentDevice = opts->device_count;
     					break;
     				}
    
  • src/local.c+8 4 modified
    @@ -102,7 +102,9 @@ int pusb_is_tty_local(char *tty)
     	if (utent->ut_addr_v6[0] != 0) {
     		struct in_addr ipnetw;
     		ipnetw.s_addr = utent->ut_addr_v6[0];
    -		char* ipaddr = inet_ntoa(ipnetw);
    +		char ipbuf[INET_ADDRSTRLEN];
    +		const char *ipaddr = inet_ntop(AF_INET, &ipnetw, ipbuf, sizeof(ipbuf));
    +		if (ipaddr == NULL) ipaddr = "(unknown)";
     
     		log_error("Remote authentication request, host: %s, ip: %s\n", utent->ut_host, ipaddr);
     		return (-1);
    @@ -155,7 +157,7 @@ char *pusb_get_tty_from_display_server(const char *display)
     
     				DIR *d_fd = opendir(fd_path);
     				if (d_fd == NULL) {
    -					log_debug("	Determining tty by display server failed (running 'pamusb-check' as user?)\n", fd_path);
    +					log_debug("	Determining tty by display server failed on %s (running 'pamusb-check' as user?)\n", fd_path);
     
     					xfree(cmdline_path);
     					xfree(cmdline);
    @@ -250,7 +252,8 @@ char *pusb_get_tty_by_loginctl()
     	char *tty = NULL;
     	if (fgets(buf, BUFSIZ, fp) != NULL)
     	{
    -		tty = strtok(buf, "\n");
    +		char *saveptr = NULL;
    +		tty = strtok_r(buf, "\n", &saveptr);
     		log_debug("		Got tty: %s\n", tty);
     
     		if (pclose(fp))
    @@ -287,7 +290,8 @@ int pusb_is_loginctl_local()
     	char *is_remote = NULL;
     	if (fgets(buf, BUFSIZ, fp) != NULL)
     	{
    -		is_remote = strtok(buf, "\n");
    +		char *saveptr = NULL;
    +		is_remote = strtok_r(buf, "\n", &saveptr);
     		log_debug("		loginctl considers this session to be remote: %s\n", is_remote);
     
     		if (pclose(fp))
    
  • src/mem.c+13 4 modified
    @@ -15,20 +15,26 @@
      * Street, Fifth Floor, Boston, MA 02110-1301 USA
      */
     
    -#include <assert.h>
     #include "mem.h"
    +#include "log.h"
     
     void *xmalloc(size_t size)
     {
     	void *data = malloc(size);
    -	assert(data != NULL && "malloc() failed");
    +	if (data == NULL) {
    +		log_error("xmalloc: out of memory\n");
    +		abort();
    +	}
     	return (data);
     }
     
     void *xrealloc(void *ptr, size_t size)
     {
     	void *data = realloc(ptr, size);
    -	assert(data != NULL && "realloc() failed");
    +	if (data == NULL) {
    +		log_error("xrealloc: out of memory\n");
    +		abort();
    +	}
     	return (data);
     }
     
    @@ -40,6 +46,9 @@ void xfree(void *ptr)
     char *xstrdup(const char *s)
     {
     	char *data = strdup(s);
    -	assert(data != NULL && "strdup() failed");
    +	if (data == NULL) {
    +		log_error("xstrdup: out of memory\n");
    +		abort();
    +	}
     	return (data);
     }
    
  • src/pad.c+33 6 modified
    @@ -256,12 +256,12 @@ static int pusb_pad_should_update(t_pusb_options *opts, const char *user)
     
     	if (delta > opts->pad_expiration)
     	{
    -		log_debug("Pads expired %u seconds ago, updating...\n", delta - opts->pad_expiration);
    +		log_debug("Pads expired %ld seconds ago, updating...\n", (long)(delta - opts->pad_expiration));
     		return 1;
     	}
     	else
     	{
    -		log_debug("Pads were generated %u seconds ago, not updating.\n", delta);
    +		log_debug("Pads were generated %ld seconds ago, not updating.\n", (long)delta);
     		return 0;
     	}
     }
    @@ -347,7 +347,15 @@ static int pusb_pad_update(
     			return 0;
     		}
     	}
    -	pusb_pad_protect(user, fileno(f_device));
    +	if (!pusb_pad_protect(user, fileno(f_device))) {
    +		if (errno != EPERM && errno != EROFS && errno != ENOTSUP) {
    +			log_error("Failed to protect device pad (unexpected error).\n");
    +			fclose(f_device);
    +			unlink(path_device_tmp);
    +			explicit_bzero(magic, sizeof(magic));
    +			return 0;
    +		}
    +	}
     
     	{
     		int fd_sys = open(path_system_tmp, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0600);
    @@ -361,7 +369,17 @@ static int pusb_pad_update(
     			return 0;
     		}
     	}
    -	pusb_pad_protect(user, fileno(f_system));
    +	if (!pusb_pad_protect(user, fileno(f_system))) {
    +		if (errno != EPERM && errno != EROFS && errno != ENOTSUP) {
    +			log_error("Failed to protect system pad (unexpected error).\n");
    +			fclose(f_system);
    +			fclose(f_device);
    +			unlink(path_system_tmp);
    +			unlink(path_device_tmp);
    +			explicit_bzero(magic, sizeof(magic));
    +			return 0;
    +		}
    +	}
     
     	log_debug("Writing pad to the system...\n");
     	if (fwrite(magic, sizeof(uint8_t), sizeof(magic), f_system) != sizeof(magic))
    @@ -395,10 +413,19 @@ static int pusb_pad_update(
     		fclose(f_device);
     		unlink(path_system_tmp);
     		unlink(path_device_tmp);
    +		explicit_bzero(magic, sizeof(magic));
    +		return 0;
    +	}
    +	if (fsync(fileno(f_system)) != 0 || fsync(fileno(f_device)) != 0)
    +	{
    +		log_error("Failed to sync pad data to disk: %s\n", strerror(errno));
    +		fclose(f_system);
    +		fclose(f_device);
    +		unlink(path_system_tmp);
    +		unlink(path_device_tmp);
    +		explicit_bzero(magic, sizeof(magic));
     		return 0;
     	}
    -	fsync(fileno(f_system));
    -	fsync(fileno(f_device));
     	fclose(f_system);
     	fclose(f_device);
     
    
  • src/process.c+4 3 modified
    @@ -106,16 +106,17 @@ char *pusb_get_process_envvar(pid_t pid, char *var)
     
     		if (size > 0)
     		{
    -			char* variable_content = strtok(buffer, "#");
    +			char *saveptr = NULL;
    +			char *variable_content = strtok_r(buffer, "#", &saveptr);
     			while (variable_content != NULL)
     			{
     				if (strncmp(var, variable_content, strlen(var)) == 0 && variable_content[strlen(var)] == '=')
     				{
    -					output = strdup(variable_content + strlen(var) + 1); // Allocate memory and copy the content
    +					output = strdup(variable_content + strlen(var) + 1);
     					break;
     				}
     
    -				variable_content = strtok(NULL, "#");
    +				variable_content = strtok_r(NULL, "#", &saveptr);
     			}
     		}
     	}
    
  • src/tmux.c+26 10 modified
    @@ -66,39 +66,50 @@ static void pusb_tmux_escape_for_regex(const char *src, char *dst, size_t dstlen
     
     char *pusb_tmux_get_client_tty(pid_t env_pid)
     {
    -    char *tmux_details = getenv("TMUX");
    -    if (tmux_details == NULL)
    +    char *tmux_details_raw = getenv("TMUX");
    +    int from_env = (tmux_details_raw != NULL);
    +    if (!from_env)
         {
             log_debug("		No TMUX env var, checking parent process in case this is a sudo request\n");
    -        tmux_details = pusb_get_process_envvar(env_pid, "TMUX");
    -
    -        if (tmux_details == NULL)
    +        tmux_details_raw = pusb_get_process_envvar(env_pid, "TMUX");
    +        if (tmux_details_raw == NULL)
             {
                 return NULL;
             }
         }
     
    +    /* Always work on a private copy: strtok_r would otherwise corrupt the live
    +     * environment block (if from getenv) or leak the pusb_get_process_envvar
    +     * allocation (if from the process environ scan). */
    +    char *tmux_details = xstrdup(tmux_details_raw);
    +    if (!from_env)
    +        xfree(tmux_details_raw);
    +
         char *tmux_client_id = strrchr(tmux_details, ',');
         if (tmux_client_id == NULL)
         {
             log_debug("		Malformed TMUX env var (no comma), cannot get client id\n");
    +        xfree(tmux_details);
             return NULL;
         }
         tmux_client_id++; // ... to strip leading comma
         log_debug("		Got tmux_client_id: %s\n", tmux_client_id);
     
    -    char *tmux_socket_path = strtok(tmux_details, ",");
    +    char *saveptr = NULL;
    +    char *tmux_socket_path = strtok_r(tmux_details, ",", &saveptr);
         log_debug("		Got tmux_socket_path: %s\n", tmux_socket_path);
     
         if (!pusb_tmux_is_safe_socket_path(tmux_socket_path))
         {
             log_error("TMUX socket path contains invalid characters, denying.\n");
    +        xfree(tmux_details);
             return NULL;
         }
     
         if (!pusb_tmux_is_numeric_id(tmux_client_id))
         {
             log_error("TMUX client ID is not numeric, denying.\n");
    +        xfree(tmux_details);
             return NULL;
         }
     
    @@ -117,18 +128,22 @@ char *pusb_tmux_get_client_tty(pid_t env_pid)
         {
             log_error("tmux detected, but couldn't get session details. Denying since remote check impossible without it!\n");
             xfree(get_tmux_session_details_cmd);
    +        xfree(tmux_details);
             return NULL;
         }
         xfree(get_tmux_session_details_cmd);
     
         char *tmux_client_tty = NULL;
    +    char *result = NULL;
         if (fgets(buf, BUFSIZ, fp) != NULL)
         {
    -        tmux_client_tty = strtok(buf, ":");
    +        char *strtok_buf_save = NULL;
    +        tmux_client_tty = strtok_r(buf, ":", &strtok_buf_save);
             if (tmux_client_tty == NULL)
             {
                 log_error("tmux detected, but couldn't parse client tty. Denying.\n");
                 pclose(fp);
    +            xfree(tmux_details);
                 return NULL;
             }
             tmux_client_tty += 5; // cut "/dev/"
    @@ -140,16 +155,17 @@ char *pusb_tmux_get_client_tty(pid_t env_pid)
             }
     
             size_t tty_len = strlen(tmux_client_tty);
    -        char *result = xmalloc(tty_len + 1);
    +        result = xmalloc(tty_len + 1);
             memcpy(result, tmux_client_tty, tty_len + 1);
    -        return result;
         }
         else
         {
             log_error("tmux detected, but couldn't get client details. Denying since remote check impossible without it!\n");
             pclose(fp);
    -        return NULL;
         }
    +
    +    xfree(tmux_details);
    +    return result;
     }
     
     /**
    
  • tests/unit/c/process_test.c+19 0 modified
    @@ -132,6 +132,24 @@ static void test_envvar_invalid_pid(void **state)
     	assert_null(val);
     }
     
    +static void test_envvar_called_twice_no_state_bleed(void **state)
    +{
    +	(void)state;
    +	/* M-1 regression: old strtok() left global tokeniser state; a second call could
    +	 * return garbage picked up from the first call's buffer instead of re-scanning. */
    +	char *path = pusb_get_process_envvar(getpid(), "PATH");
    +	char *home = pusb_get_process_envvar(getpid(), "HOME");
    +
    +	assert_non_null(path);
    +	assert_true(strchr(path, '/') != NULL);
    +
    +	assert_non_null(home);
    +	assert_true(home[0] == '/');
    +
    +	free(path);
    +	free(home);
    +}
    +
     /* ── main ── */
     
     int main(void)
    @@ -155,6 +173,7 @@ int main(void)
     		cmocka_unit_test(test_envvar_path_found),
     		cmocka_unit_test(test_envvar_nonexistent),
     		cmocka_unit_test(test_envvar_invalid_pid),
    +		cmocka_unit_test(test_envvar_called_twice_no_state_bleed),
     	};
     	return cmocka_run_group_tests(tests, NULL, NULL);
     }
    
  • tests/unit/c/tmux_test.c+25 0 modified
    @@ -243,6 +243,30 @@ static void test_get_client_tty_uses_absolute_tmux_path(void **state)
     	unsetenv("TMUX");
     }
     
    +static void test_get_client_tty_does_not_corrupt_tmux_env(void **state)
    +{
    +	(void)state;
    +	/* M-1 regression: the old code called strtok() directly on the getenv("TMUX")
    +	 * pointer, inserting NUL bytes into the live environment block and permanently
    +	 * corrupting the variable for the rest of the process lifetime. */
    +	const char *original = "/tmp/tmux-1000/default,abc,42";
    +	setenv("TMUX", original, 1);
    +
    +	g_popen_output = "/dev/pts/1: session info\n";
    +	char *result = pusb_tmux_get_client_tty(0);
    +
    +	assert_non_null(result);
    +	assert_string_equal("pts/1", result);
    +	free(result);
    +
    +	/* Environment variable must be byte-for-byte intact after the call */
    +	const char *after = getenv("TMUX");
    +	assert_non_null(after);
    +	assert_string_equal(original, after);
    +
    +	unsetenv("TMUX");
    +}
    +
     /* ── main ── */
     
     int main(void)
    @@ -277,6 +301,7 @@ int main(void)
     		cmocka_unit_test(test_get_client_tty_nonnumeric_id),
     		cmocka_unit_test(test_get_client_tty_valid),
     		cmocka_unit_test(test_get_client_tty_uses_absolute_tmux_path),
    +		cmocka_unit_test(test_get_client_tty_does_not_corrupt_tmux_env),
     	};
     	return cmocka_run_group_tests(tests, NULL, NULL);
     }
    

Vulnerability mechanics

Root cause

"Use of non-reentrant strtok() in multi-threaded PAM authentication context causes race conditions in remote-session detection, and passing getenv() pointer directly to strtok() permanently corrupts the process environment block."

Attack vector

An attacker with local shell access can exploit this by triggering concurrent authentication attempts (e.g., via multiple sudo or login prompts) in a display manager like GDM that runs multiple authentication threads. The non-reentrant `strtok()` stores state in a single global pointer; if two threads race, one thread's `strtok()` call overwrites the other's in-progress tokenisation pointer, causing incorrect parsing of tmux session data or the `/proc` environ scan that backs the remote-session detection logic [ref_id=1]. Additionally, `pusb_tmux_get_client_tty()` passed the raw pointer from `getenv("TMUX")` directly to `strtok()`, which inserts NUL bytes into the live environment block, permanently corrupting the `TMUX` variable for all future authentications in the same process [ref_id=1]. The combined effect can cause `deny_remote=true` to return an incorrect decision for a remote or local session depending on thread interleaving.

Affected code

The vulnerability spans multiple source files. In `src/tmux.c`, `pusb_tmux_get_client_tty()` called the non-reentrant `strtok()` directly on the pointer returned by `getenv("TMUX")`, which points into the live process environment block [patch_id=2779106]. In `src/process.c`, `pusb_get_process_envvar()` also used `strtok()` for parsing `/proc/PID/environ` data [patch_id=2779106]. In `src/local.c`, `pusb_get_tty_by_loginctl()` and `pusb_is_loginctl_local()` similarly used `strtok()` [patch_id=2779106]. The `deny_remote` feature relies on these functions to detect remote sessions.

What the fix does

The fix in `src/tmux.c` replaces all `strtok()` calls with the reentrant `strtok_r()`, which uses an explicit `saveptr` argument instead of a global pointer, eliminating the race condition between concurrent authentication threads [patch_id=2779106]. It also makes a private copy of the `TMUX` environment variable via `xstrdup()` before tokenising, so that `strtok_r()` inserts NUL bytes into the copy rather than corrupting the live process environment block [patch_id=2779106]. The same `strtok()` → `strtok_r()` replacement is applied in `src/process.c` and `src/local.c` [patch_id=2779106]. The `pad.c` changes (patch_id=2779105) address a separate TOCTOU issue between `lstat()` and `open()` by introducing `open_pad_file_in_dir()` which opens the parent directory with `O_DIRECTORY|O_NOFOLLOW` and uses `openat()` to prevent symlink races, and add snprintf truncation checks to reject overly long paths instead of silently truncating them.

Preconditions

  • authAttacker must have local shell access to the system
  • configThe pam_usb module must be configured with deny_remote=true
  • configThe display manager (e.g., GDM) must run multiple concurrent authentication threads
  • inputFor the TMUX env corruption path, the target user must be running inside a tmux session

Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.