CVE-2026-48066
Description
pam_usb provides hardware authentication for Linux using ordinary removable media. Prior to 0.9.1, src/log.c contains a process-wide static pointer that is written on every PAM invocation with the address of a stack-local variable. This violates the PAM re-entrancy requirement and creates a data race when the PAM stack is invoked concurrently from multiple threads. This vulnerability is fixed in 0.9.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
pam_usb before 0.9.1 uses a process-wide static pointer that stores a stack-local address, breaking PAM re-entrancy and causing data races under concurrent authentication.
Vulnerability
pam_usb versions prior to 0.9.1 contain a thread-safety vulnerability in src/log.c. A file-scoped static pointer static t_pusb_options *pusb_opts = NULL is written on every PAM invocation with the address of a stack-local variable via pusb_log_init(). This violates the PAM re-entrancy requirement because PAM modules may be invoked concurrently from multiple threads in a multi-threaded application [1], [2]. The vulnerable code path is reachable when any application uses pam_usb for authentication (e.g., via pam_sm_authenticate or pam_sm_acct_mgmt), and no special configuration is required to trigger the race condition [1].
Exploitation
An attacker does not need network access or authentication. The vulnerability is a data race that can be triggered whenever two or more threads simultaneously invoke pam_usb's PAM functions [1], [2]. The sequence is: (1) Thread A calls pusb_log_init(&opts_A) → pusb_opts points to Thread A's stack. (2) Thread B calls pusb_log_init(&opts_B) → pusb_opts is overwritten to point to Thread B's stack. (3) Thread A's subsequent calls to log_debug() or log_error() now read settings (debug, color_log, quiet) from Thread B's stack frame. (4) If Thread B returns before Thread A finishes logging, the pusb_opts pointer becomes dangling, leading to undefined behavior [1], [2].
Impact
A successful exploitation can cause: (a) log verbosity settings from a racing thread to be used, resulting in incorrect logging output or silent suppression of errors; (b) a crash of the calling process due to dereferencing a dangling stack pointer [1], [2]. This constitutes a denial-of-service for authentication, potentially preventing legitimate logins in multi-threaded environments (e.g., a login service or parallel su calls) [1], [2]. No privilege escalation or information disclosure beyond log data corruption has been documented in the references.
Mitigation
The issue is fixed in pam_usb version 0.9.1 by adding the C11 _Thread_local storage class to the static pointer declaration in src/log.c, making the variable thread-local and preserving re-entrancy [1], [2]. Users should upgrade to 0.9.1 or later. No workaround is provided for older versions, as the fix is a one-line code change [1]. The vulnerability is not listed on CISA's Known Exploited Vulnerabilities (KEV) as of the publication date.
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
3e2f30b22cd07Prepare 0.9.1 release (#362)
7 files changed · +37 −7
arch_linux/PKGBUILD_stable+1 −1 modified@@ -2,7 +2,7 @@ # Contributor: Pekka Helenius <fincer89 [at] hotmail [dot] com> pkgname=pam_usb -pkgver=0.9.0 +pkgver=0.9.1 pkgrel=1 pkgdesc='Hardware authentication for Linux using ordinary flash media (USB & Card based).' arch=($CARCH)
ChangeLog+9 −0 modified@@ -1,3 +1,12 @@ +* 0.9.1 + [Bugfix] Restore debug output suppressed before config parsing; fix XRDP detection log message (#355) + [Security] Fixed GHSA-qg76-57wq-mpv6 (thread-unsafe static pointer in log.c) (#355) + [Security] Fixed GHSA-24mw-m2vf-36vp (integer overflow before heap allocation in conf.c) (#359) + [Security] Fixed GHSA-pvrg-chgw-x42c (evdev scan silently discards EACCES under non-root) (#360) + [Security] Fixed GHSA-w38v-cw9r-x9p6 (PAM_RHOST check skipped when deny_remote=false) (#361) + [CI/Tests] Add regression tests for thread-safe log.c (#357) + [Refactor] Deduplicate pam_sm_authenticate / pam_sm_acct_mgmt in pam.c (#356) + * 0.9.0 [Feature] Remove hardcoded 10-device limit per user (#236) [Feature] Add additional remote connection check for VNC/RDP (optional, default on) (#202)
debian/changelog+12 −0 modified@@ -1,3 +1,15 @@ +libpam-usb (0.9.1) unstable; urgency=high + + * [Bugfix] Restore debug output suppressed before config parsing; fix XRDP detection log message (#355) + * [Security] Fixed GHSA-qg76-57wq-mpv6 (thread-unsafe static pointer in log.c) (#355) + * [Security] Fixed GHSA-24mw-m2vf-36vp (integer overflow before heap allocation in conf.c) (#359) + * [Security] Fixed GHSA-pvrg-chgw-x42c (evdev scan silently discards EACCES under non-root) (#360) + * [Security] Fixed GHSA-w38v-cw9r-x9p6 (PAM_RHOST check skipped when deny_remote=false) (#361) + * [CI/Tests] Add regression tests for thread-safe log.c (#357) + * [Refactor] Deduplicate pam_sm_authenticate / pam_sm_acct_mgmt in pam.c (#356) + + -- Tobias Bäumer <tobiasbaeumer@gmail.com> Wed, 20 May 2026 00:00:00 +0200 + libpam-usb (0.9.0) unstable; urgency=critical * [Feature] Remove hardcoded 10-device limit per user (#236) * [Feature] Add additional remote connection check for VNC/RDP (optional, default on) (#202)
fedora/SPECS/pam_usb.spec+10 −1 modified@@ -1,7 +1,7 @@ %define _topdir /usr/local/src/pam_usb/fedora %define name pam_usb %define release 1 -%define version 0.9.0 +%define version 0.9.1 %define buildroot %{_topdir}/%{name}‑%{version}‑root BuildRoot: %{buildroot} @@ -62,6 +62,15 @@ rm -rf %{buildroot}/usr/share/pam-configs %changelog +* Wed May 20 2026 Tobias Bäumer <tobiasbaeumer@gmail.com> - 0.9.1 +- [Bugfix] Restore debug output suppressed before config parsing; fix XRDP detection log message (#355) +- [Security] Fixed GHSA-qg76-57wq-mpv6 (thread-unsafe static pointer in log.c) (#355) +- [Security] Fixed GHSA-24mw-m2vf-36vp (integer overflow before heap allocation in conf.c) (#359) +- [Security] Fixed GHSA-pvrg-chgw-x42c (evdev scan silently discards EACCES under non-root) (#360) +- [Security] Fixed GHSA-w38v-cw9r-x9p6 (PAM_RHOST check skipped when deny_remote=false) (#361) +- [CI/Tests] Add regression tests for thread-safe log.c (#357) +- [Refactor] Deduplicate pam_sm_authenticate / pam_sm_acct_mgmt in pam.c (#356) + * Tue May 19 2026 Tobias Bäumer <tobiasbaeumer@gmail.com> - 0.9.0 - [Feature] Remove hardcoded 10-device limit per user (#236) - [Feature] Add additional remote connection check for VNC/RDP (optional, default on) (#202)
SECURITY.md+3 −3 modified@@ -6,8 +6,8 @@ At every point in time only the most recent release is supported. | Version | Supported | |---------| ------------------ | -| 0.9.0 | ✅ | -| < 0.9.0 | ❌ | +| 0.9.1 | ✅ | +| < 0.9.1 | ❌ | ## Reporting a Vulnerability @@ -29,4 +29,4 @@ Once a vulnerability has been reported via email and coordinated with the mainta There are multiple published security advisories for these versions. -If you're running anything except 0.9.0 please update immediately! +If you're running anything except 0.9.1 please update immediately!
src/version.h+1 −1 modified@@ -18,6 +18,6 @@ #ifndef PUSB_VERSION_H_ # define PUSB_VERSION_H_ -# define PUSB_VERSION "0.9.0" +# define PUSB_VERSION "0.9.1" #endif /* !PUSB_VERSION_H_ */
tools/pamusb-conf+1 −1 modified@@ -421,7 +421,7 @@ def resetPads(): sys.exit(0) def usage(): - print('Version 0.9.0') + print('Version 0.9.1') print('Usage: %s [--help] [--verbose] [--yes] [--config=path] [--reset-pads=username]' % os.path.basename(__file__)) print(' [[--add-user=name [--superuser]] | [--add-device=name [[--device=number] [--volume=number]]]]') sys.exit(1)
76c4a5727e1b[Test] Add regression tests for thread-safe log.c (issue #350) (#357)
2 files changed · +135 −1
Makefile+7 −1 modified@@ -115,6 +115,7 @@ RMSVC_LDFLAGS := -lcmocka EVDEV_LDFLAGS := -lcmocka LOCAL_LDFLAGS := `pkg-config --libs libevdev` -lcmocka PAM_TEST_LDFLAGS := -lcmocka +LOG_TEST_LDFLAGS := -lcmocka test-c-xpath: src/xpath.o src/mem.o src/log.o $(CC) $(TEST_CFLAGS) tests/unit/c/xpath_test.c $^ $(XPATH_LDFLAGS) -o tests/unit/c/xpath_test @@ -159,7 +160,12 @@ test-c-pam: src/log.o $(PAM_TEST_LDFLAGS) -o tests/unit/c/pam_test ./tests/unit/c/pam_test -test-c: test-c-xpath test-c-conf test-c-tmux test-c-pad test-c-process test-c-rmsvc test-c-local test-c-evdev test-c-pam +test-c-log: tests/unit/c/log_test.c src/log.c + $(CC) $(TEST_CFLAGS) tests/unit/c/log_test.c \ + $(LOG_TEST_LDFLAGS) -o tests/unit/c/log_test + ./tests/unit/c/log_test + +test-c: test-c-xpath test-c-conf test-c-tmux test-c-pad test-c-process test-c-rmsvc test-c-local test-c-evdev test-c-pam test-c-log test-python: python3 -m pytest tests/unit/python/ -v
tests/unit/c/log_test.c+128 −0 added@@ -0,0 +1,128 @@ +/* + * Unit tests for src/log.c + * Tests pusb_log_init state management: null safety, value copying, and + * sequential override. The critical test (copies_not_pointer) directly + * guards against regression to the old static-pointer pattern that caused + * the thread-safety bug fixed in #355 (issue #350). + * + * log.c is included directly to access the static thread-local variables. + */ + +#include <stdarg.h> +#include <stddef.h> +#include <setjmp.h> +#include <string.h> +#include <cmocka.h> + +#include "../../../src/log.c" + +/* ── pusb_log_init(NULL) ── */ + +static void test_log_init_null_zeroes_all(void **state) +{ + (void)state; + pusb_log_debug = 1; + pusb_log_quiet = 1; + pusb_log_color = 1; + + pusb_log_init(NULL); + + assert_int_equal(0, pusb_log_debug); + assert_int_equal(0, pusb_log_quiet); + assert_int_equal(0, pusb_log_color); +} + +/* ── pusb_log_init(&opts) copies individual fields ── */ + +static void test_log_init_sets_debug(void **state) +{ + (void)state; + t_pusb_options opts; + memset(&opts, 0, sizeof(opts)); + opts.debug = 1; + pusb_log_init(&opts); + assert_int_equal(1, pusb_log_debug); + assert_int_equal(0, pusb_log_quiet); + assert_int_equal(0, pusb_log_color); +} + +static void test_log_init_sets_quiet(void **state) +{ + (void)state; + t_pusb_options opts; + memset(&opts, 0, sizeof(opts)); + opts.quiet = 1; + pusb_log_init(&opts); + assert_int_equal(0, pusb_log_debug); + assert_int_equal(1, pusb_log_quiet); + assert_int_equal(0, pusb_log_color); +} + +static void test_log_init_sets_color(void **state) +{ + (void)state; + t_pusb_options opts; + memset(&opts, 0, sizeof(opts)); + opts.color_log = 1; + pusb_log_init(&opts); + assert_int_equal(0, pusb_log_debug); + assert_int_equal(0, pusb_log_quiet); + assert_int_equal(1, pusb_log_color); +} + +/* ── Key regression guard: values are copied, not pointed to ── + * + * The bug fixed in #355 stored &opts (stack) in a process-wide static. + * This test proves the current implementation copies field values: mutating + * opts after pusb_log_init() must NOT change the log state. + */ +static void test_log_init_copies_not_pointer(void **state) +{ + (void)state; + t_pusb_options opts; + memset(&opts, 0, sizeof(opts)); + opts.debug = 1; + opts.quiet = 1; + opts.color_log = 1; + + pusb_log_init(&opts); + + /* Mutate the source struct — internal log state must NOT follow */ + opts.debug = 0; + opts.quiet = 0; + opts.color_log = 0; + + assert_int_equal(1, pusb_log_debug); + assert_int_equal(1, pusb_log_quiet); + assert_int_equal(1, pusb_log_color); +} + +/* ── Sequential calls overwrite previous values ── */ + +static void test_log_init_sequential_override(void **state) +{ + (void)state; + t_pusb_options opts; + memset(&opts, 0, sizeof(opts)); + + opts.debug = 1; + pusb_log_init(&opts); + assert_int_equal(1, pusb_log_debug); + + opts.debug = 0; + pusb_log_init(&opts); + assert_int_equal(0, pusb_log_debug); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_log_init_null_zeroes_all), + cmocka_unit_test(test_log_init_sets_debug), + cmocka_unit_test(test_log_init_sets_quiet), + cmocka_unit_test(test_log_init_sets_color), + cmocka_unit_test(test_log_init_copies_not_pointer), + cmocka_unit_test(test_log_init_sequential_override), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +}
13eb33c0345f[Fix] Restore debug output suppressed before config parsing; fix XRDP log message (#355)
6 files changed · +63 −8
src/local.c+1 −1 modified@@ -342,7 +342,7 @@ int pusb_local_login(t_pusb_options *opts, const char *user, const char *service char *xrdpSession = getenv("XRDP_SESSION"); if (xrdpSession != NULL) { - log_error("XRDP session detected, denying.\n", xrdpSession); + log_error("XRDP session detected (%s), denying.\n", xrdpSession); return (0); }
src/log.c+11 −7 modified@@ -22,7 +22,9 @@ #include "conf.h" #include "log.h" -static t_pusb_options *pusb_opts = NULL; +static _Thread_local int pusb_log_debug = 0; +static _Thread_local int pusb_log_quiet = 0; +static _Thread_local int pusb_log_color = 0; static void pusb_log_syslog(int level, const char *format, va_list ap) { @@ -33,14 +35,14 @@ static void pusb_log_syslog(int level, const char *format, va_list ap) static void pusb_log_output(int level, const char *format, va_list ap) { - if (!isatty(fileno(stdin))) + if (!isatty(fileno(stdin))) { return; } - - if (pusb_opts && !pusb_opts->quiet) + + if (!pusb_log_quiet) { - if (pusb_opts && pusb_opts->color_log) + if (pusb_log_color) { if (level == LOG_ERR) { @@ -64,7 +66,7 @@ void __log_debug(const char *file, int line, const char *fmt, ...) { va_list ap; - if (!pusb_opts || !pusb_opts->debug) + if (!pusb_log_debug) { return; } @@ -107,5 +109,7 @@ void log_info(const char *fmt, ...) void pusb_log_init(t_pusb_options *opts) { - pusb_opts = opts; + pusb_log_debug = opts ? opts->debug : 0; + pusb_log_quiet = opts ? opts->quiet : 0; + pusb_log_color = opts ? opts->color_log : 0; }
src/pam.c+2 −0 modified@@ -71,6 +71,7 @@ int pam_sm_authenticate( { return PAM_AUTH_ERR; } + pusb_log_init(&opts); if (!opts.enable) { @@ -171,6 +172,7 @@ int pam_sm_acct_mgmt( { return PAM_AUTH_ERR; } + pusb_log_init(&opts); if (!opts.enable) {
tests/can-actually-be-used/run-tests.sh+1 −0 modified@@ -17,5 +17,6 @@ rm -rf /home/`whoami`/.pamusb ./test-check-many-devices.sh && \ ./test-check-superuser-filtering.sh && \ ./test-conf-adds-user-with-superuser.sh && \ +./test-check-deny-xrdp-session.sh && \ rm -rf /tmp/fakestick/.pamusb && \ ./test-agent-properly-triggers.sh
tests/can-actually-be-used/test-check-deny-xrdp-session.sh+32 −0 added@@ -0,0 +1,32 @@ +#!/usr/bin/bash +# +# Regression test for local.c: XRDP_SESSION env var must cause denial. +# The check fires in pusb_local_login() before any device or TTY inspection, +# so no USB device is required for this test. +# +# Note: CI disables deny_remote globally so SSH-based pamusb-check calls work. +# We create a temp config with deny_remote=true to test this path in isolation. +# We also check syslog for the message content because log_error() only writes +# to stderr when stdin is a TTY (see log.c:pusb_log_output); in CI (non-interactive +# SSH) stdin is not a TTY, so the message only appears in syslog. + +WHOAMI=$(whoami) +CONF=/etc/security/pam_usb.conf +TMP_CONF=$(mktemp /tmp/pam_usb_xrdp_test_XXXXXX.conf) +trap 'rm -f "$TMP_CONF"' EXIT + +# Flip deny_remote to true (CI sets it to false globally to allow SSH-based checks) +sed 's|<option name="deny_remote">false</option>|<option name="deny_remote">true</option>|' "$CONF" > "$TMP_CONF" + +echo -e "Test:\t\t\tpamusb-check denies access when XRDP_SESSION is set" +if XRDP_SESSION=1 pamusb-check --config="$TMP_CONF" "$WHOAMI" 2>/dev/null; then + echo "FAILED: pamusb-check should deny access when XRDP_SESSION is set" + exit 1 +fi +echo -e "Result:\t\t\tPASSED!" + +echo -e "Test:\t\t\tpamusb-check log contains XRDP session message" +XRDP_SESSION=testvalue pamusb-check --config="$TMP_CONF" "$WHOAMI" 2>/dev/null || true +journalctl -t pam_usb --no-pager -n 20 2>/dev/null | grep -q "XRDP session detected (testvalue)" && \ + echo -e "Result:\t\t\tPASSED!" || \ + { echo "FAILED: expected 'XRDP session detected (testvalue)' in syslog"; exit 1; }
tests/unit/c/local_test.c+16 −0 modified@@ -7,6 +7,7 @@ #include <stdarg.h> #include <stddef.h> #include <setjmp.h> +#include <stdlib.h> #include <string.h> #include <cmocka.h> @@ -102,6 +103,20 @@ static void test_utmpx_field_starts_with_rejects_long_prefix(void **state) assert_int_equal(0, pusb_utmpx_field_starts_with(field, sizeof(field), "pts/")); } +static void test_local_login_denies_xrdp_session(void **state) +{ + (void)state; + t_pusb_options opts; + memset(&opts, 0, sizeof(opts)); + opts.deny_remote = 1; + + setenv("XRDP_SESSION", "1", 1); + int result = pusb_local_login(&opts, "testuser", "testservice"); + unsetenv("XRDP_SESSION"); + + assert_int_equal(0, result); +} + int main(void) { const struct CMUnitTest tests[] = { @@ -115,6 +130,7 @@ int main(void) cmocka_unit_test(test_utmpx_field_starts_with_pts_slave), cmocka_unit_test(test_utmpx_field_starts_with_full_width_field), cmocka_unit_test(test_utmpx_field_starts_with_rejects_long_prefix), + cmocka_unit_test(test_local_login_denies_xrdp_session), }; return cmocka_run_group_tests(tests, NULL, NULL); }
Vulnerability mechanics
Root cause
"A process-wide static pointer in src/log.c stores the address of a stack-local variable, violating PAM re-entrancy and creating a data race under concurrent authentication."
Attack vector
An attacker who can cause concurrent PAM authentication invocations (e.g., via a multi-threaded login service or parallel `su` calls) triggers a data race on the global `pusb_opts` pointer [ref_id=1][ref_id=2]. Thread A calls `pusb_log_init(&opts_A)` setting the global to its stack address, then Thread B clobbers it with its own stack address [ref_id=1][ref_id=2]. Thread A's subsequent `log_debug()`/`log_error()` calls read from Thread B's stack frame, and after Thread B returns the pointer dangles — causing undefined behavior and potential crash [ref_id=1][ref_id=2]. The attacker needs no special privileges (PR:N) but requires local access and the ability to trigger concurrent PAM authentications (AV:L, AC:H) [ref_id=1].
Affected code
The defect is in `src/log.c:25` where a process-wide static pointer `static t_pusb_options *pusb_opts = NULL` is declared [ref_id=1][ref_id=2]. This pointer is set via `pusb_log_init()` (line 110) which is called with a stack-local `t_pusb_options` at the start of both `pam_sm_authenticate` (`pam.c:45`) and `pam_sm_acct_mgmt` (`pam.c:145`) [ref_id=1][ref_id=2].
What the fix does
The fix changes the storage class of `pusb_opts` in `src/log.c:25` from `static` to `static _Thread_local` [ref_id=1][ref_id=2]. This C11 keyword gives each thread its own independent copy of the pointer, zero-initialised per thread, so concurrent invocations no longer clobber each other's state [ref_id=1]. No other code changes are needed because `_Thread_local` preserves the existing `pusb_log_init` contract exactly [ref_id=1].
Preconditions
- configThe host process must invoke the PAM stack from multiple threads concurrently (e.g., multi-threaded login service or parallel su calls)
- networkAttacker must have local access to the system to trigger concurrent PAM authentication attempts
- authNo authentication required — the race is triggered during the authentication flow itself
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.