CVE-2026-47271
Description
pam_usb provides hardware authentication for Linux using ordinary removable media. Prior to 0.9.0, src/mem.c implemented out-of-memory guards for xmalloc(), xrealloc(), and xstrdup() using assert(data != NULL). The C standard specifies that all assert() expressions are compiled out when NDEBUG is defined at build time. NDEBUG is commonly defined in release and packaging builds (Debian, Fedora, Arch package flags all define it via -DNDEBUG in CFLAGS). With the guard removed, xmalloc/xrealloc/xstrdup silently return NULL on allocation failure. Every caller in the codebase dereferences the return value without a NULL check -- this is the intended design, as the guard was supposed to abort before the dereference. With the guard gone, any allocation failure causes a NULL pointer dereference, crashing the PAM module. A crash in a PAM module loaded by sudo or login causes authentication to fail for the duration of the crash, creating a local denial-of-service condition. An attacker who can induce memory pressure at authentication time can lock all users out of sudo and login. This vulnerability is fixed in 0.9.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
pam_usb uses assert() for OOM guards; when NDEBUG is defined, the guards vanish and allocation failures cause a NULL dereference, crashing the PAM module and enabling local DoS.
Vulnerability
In pam_usb before 0.9.0, the memory allocation functions xmalloc(), xrealloc(), and xstrdup() in src/mem.c used assert(data != NULL) to guard against out-of-memory (OOM) conditions. According to the C standard, all assert() expressions are compiled out when NDEBUG is defined at build time. NDEBUG is commonly defined in release and packaging builds (e.g., Debian, Fedora, Arch package flags all set -DNDEBUG in CFLAGS). With the guard removed, allocation failure silently returns NULL. Every caller in the codebase dereferences the return value without a NULL check, relying on the guard to abort before the dereference. The vulnerability therefore exists in all versions prior to 0.9.0 [1][2].
Exploitation
An attacker who can induce memory pressure at the time of authentication (e.g., by exhausting system memory or by triggering a low-memory condition) can cause any memory allocation inside the pam_usb module to fail. Because the OOM guards are absent in builds with NDEBUG, xmalloc(), xrealloc(), or xstrdup() will return NULL, and the subsequent dereference in one of the many callers in the codebase will cause a NULL pointer dereference. The crash occurs in the PAM module context, which is loaded by privileged processes such as sudo or login. The attacker does not require any special privileges other than the ability to affect memory pressure on the system [2].
Impact
A successful denial-of-service attack causes the pam_usb PAM module to crash. Because sudo and login typically call PAM authentication synchronously, a crash in the module causes authentication to fail for the duration of the crash—no user, including root, can authenticate via those services. A local attacker can therefore lock all users out of sudo and login, creating a local denial-of-service condition. The privilege level required is low (local unprivileged user). The impact is limited to availability; there is no evidence of privilege escalation or information disclosure beyond the crash [2].
Mitigation
The vulnerability is fixed in pam_usb version 0.9.0. The fix (commit d003e551b794a9e3774ff4720830fb7aadaa48bd) replaces assert() with an explicit abort path that includes an OOM check unaffected by NDEBUG. Users should upgrade to 0.9.0 or later. No workaround is available for versions using NDEBUG builds; users who compile from source may remove the -DNDEBUG flag, but this is not recommended for production as it may have other side effects. The vulnerability is not known to be listed in CISA KEV [1][2].
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.
Patches
1d003e551b794Security audit round 3: fix 8 findings across 6 source files
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
"Out-of-memory guards in xmalloc(), xrealloc(), and xstrdup() used assert(), which is compiled out when NDEBUG is defined, causing these functions to silently return NULL on allocation failure and leading to a NULL pointer dereference."
Attack vector
An attacker induces memory pressure on the system while a user authenticates via sudo or login. When `xmalloc()`, `xrealloc()`, or `xstrdup()` fail due to allocation failure, the removed `assert()` guard (compiled out by `-DNDEBUG`) causes the function to silently return NULL. Every caller dereferences the return value without a NULL check, triggering a NULL pointer dereference that crashes the PAM module. Because the PAM module is loaded into the sudo/login process, the crash causes authentication to fail for the duration of the crash, creating a local denial-of-service condition [ref_id=2].
Affected code
The vulnerability is in `src/mem.c` in the functions `xmalloc()`, `xrealloc()`, and `xstrdup()`. These functions used `assert(data != NULL)` as their out-of-memory guard. The patch replaces those assertions with explicit `if (data == NULL) { log_error(...); abort(); }` blocks [patch_id=2749082].
What the fix does
The patch replaces `assert(data != NULL)` in `xmalloc()`, `xrealloc()`, and `xstrdup()` with an explicit `if (data == NULL) { log_error(...); abort(); }` block [patch_id=2749082]. Unlike `assert()`, this code is never compiled out regardless of whether `NDEBUG` is defined. The `abort()` call ensures the process terminates immediately on allocation failure, preserving the original design intent that callers never need to NULL-check the return value [ref_id=2].
Preconditions
- configThe pam_usb PAM module must be loaded by sudo, login, or another authentication service.
- configThe build must define NDEBUG (common in Debian, Fedora, Arch packaging builds).
- inputThe attacker must be able to induce memory pressure on the system at the time of authentication.
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.