VYPR
High severity7.1NVD Advisory· Published May 27, 2026

CVE-2026-47272

CVE-2026-47272

Description

pam_usb provides hardware authentication for Linux using ordinary removable media. Prior to 0.9.0, the pusb_pad_compare() function in src/pad.c only verified that the user-side pad (~/.pamusb/device.pad) could be read, but did not enforce that the system-side pad (the pad file on the USB device) was also present and readable. If the user-side pad was deleted or unreadable, the function returned a failure that was treated as non-fatal in certain code paths, allowing authentication to succeed without the USB device being verified. A local user can delete their own ~/.pamusb/device.pad to remove the USB device requirement and authenticate without the physical device. This vulnerability is fixed in 0.9.0.

AI Insight

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

Missing system-side pad verification in pam_usb before 0.9.0 lets local users bypass USB authentication by deleting their user-side pad file.

Vulnerability

The pusb_pad_compare() function in src/pad.c of pam_usb before version 0.9.0 only verified that the user-side pad (~/.pamusb/device.pad) could be read, but did not enforce that the system-side pad (the pad file on the USB device) was also present and readable [1]. If the user-side pad was deleted or unreadable, the function returned a failure that was treated as non-fatal in certain code paths, allowing authentication to succeed without the USB device being verified.

Exploitation

An attacker with local access can delete their own ~/.pamusb/device.pad file. This causes pusb_pad_compare() to fail when attempting to read the user-side pad, but due to the asymmetric verification logic, the failure is not treated as a fatal error, and authentication proceeds without checking the USB device. No additional privileges or user interaction are required beyond local shell access to the targeted account.

Impact

A local user can bypass hardware authentication and log in without the physical USB device. This undermines the two-factor security model of pam_usb, reducing it to simple password authentication. The attacker gains unauthorized access to the system as the target user, potentially leading to privilege escalation or data compromise depending on the user's permissions.

Mitigation

The vulnerability is fixed in pam_usb version 0.9.0 [1]. Users should upgrade to this version or later. There is no known workaround; ensuring that both pad files are properly synchronized and the user cannot modify ~/.pamusb/device.pad may mitigate some cases, but the only complete fix is upgrading to the patched release. This CVE is not listed in CISA's Known Exploited Vulnerabilities (KEV) 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.

Patches

1
275f0ebc7c00

#301 Harden OTP pad mechanism (F1–F5, F7, F9) (#303)

https://github.com/mcdope/pam_usbMcDopeMay 10, 2026via body-scan-shorthand
4 files changed · +228 61
  • src/pad.c+101 60 modified
    @@ -18,10 +18,12 @@
     #include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
    +#include <stdint.h>
     #include <unistd.h>
     #include <errno.h>
     #include <sys/types.h>
     #include <sys/stat.h>
    +#include <sys/random.h>
     #include <fcntl.h>
     #include <pwd.h>
     #include <time.h>
    @@ -31,6 +33,8 @@
     #include "volume.h"
     #include "pad.h"
     
    +#define PUSB_PAD_SIZE 1024
    +
     static int pusb_pad_build_device_path(
     	t_pusb_options *opts,
     	const char *mnt_point,
    @@ -154,7 +158,7 @@ static FILE *pusb_pad_open_device(
     	{
     		return NULL;
     	}
    -	int fd = open(path, flags | O_NOFOLLOW, 0600);
    +	int fd = open(path, flags | O_NOFOLLOW | O_CLOEXEC, 0600);
     	if (fd < 0)
     	{
     		log_debug("Cannot open device file: %s\n", strerror(errno));
    @@ -183,7 +187,7 @@ static FILE *pusb_pad_open_system(
     	{
     		return NULL;
     	}
    -	int fd = open(path, flags | O_NOFOLLOW, 0600);
    +	int fd = open(path, flags | O_NOFOLLOW | O_CLOEXEC, 0600);
     	if (fd < 0)
     	{
     		log_debug("Cannot open system file: %s\n", strerror(errno));
    @@ -211,7 +215,7 @@ static int pusb_pad_protect(const char *user, int fd)
     	}
     	if (fchown(fd, user_ent->pw_uid, user_ent->pw_gid) == -1)
     	{
    -		log_debug("Unable to change owner of the pad: %s\n", strerror(errno));
    +		log_debug("Unable to change owner of the pad: %s (expected on filesystems not supporting this, like FAT)\n", strerror(errno));
     		return 0;
     	}
     	if (fchmod(fd, S_IRUSR | S_IWUSR) == -1)
    @@ -260,7 +264,33 @@ static int pusb_pad_should_update(t_pusb_options *opts, const char *user)
     		log_debug("Pads were generated %u seconds ago, not updating.\n", delta);
     		return 0;
     	}
    -	return 1;
    +}
    +
    +/*
    + * generate_random_bytes — fill buf with len cryptographically strong random
    + * bytes via getrandom(2). Retries on EINTR. Returns 0 on success, -1 on error.
    + * Since Linux 5.6 the urandom and random pools are identical; getrandom(2)
    + * is the correct modern interface (no fd, no path, never blocks after boot).
    + * Requests > 256 bytes may return a short count when interrupted, hence the
    + * retry loop.
    + */
    +static int generate_random_bytes(uint8_t *buf, size_t len)
    +{
    +	size_t offset = 0;
    +
    +	while (offset < len)
    +	{
    +		ssize_t ret = getrandom(buf + offset, len - offset, 0);
    +		if (ret < 0)
    +		{
    +			if (errno == EINTR)
    +				continue;
    +			log_error("getrandom() failed: %s\n", strerror(errno));
    +			return -1;
    +		}
    +		offset += (size_t)ret;
    +	}
    +	return 0;
     }
     
     static int pusb_pad_update(
    @@ -275,9 +305,7 @@ static int pusb_pad_update(
     	char path_system[1024*5];
     	char path_device_tmp[1024*5 + 8];
     	char path_system_tmp[1024*5 + 8];
    -	char magic[1024];
    -	unsigned int seed;
    -	int devrandom;
    +	uint8_t magic[PUSB_PAD_SIZE];
     
     	if (!pusb_pad_should_update(opts, user))
     	{
    @@ -301,76 +329,81 @@ static int pusb_pad_update(
     	snprintf(path_device_tmp, sizeof(path_device_tmp), "%s.tmp", path_device);
     	snprintf(path_system_tmp, sizeof(path_system_tmp), "%s.tmp", path_system);
     
    -	log_debug("Generating %d bytes unique pad...\n", sizeof(magic));
    -	/**
    -	 * In case you wonder, how I did, if this should use /dev/urandom instead: no, /dev/random is correct in this case
    -	 * See https://crypto.stackexchange.com/a/35032
    -	 */
    -	devrandom = open("/dev/random", O_RDONLY);
    -	if (devrandom < 0 || read(devrandom, &seed, sizeof seed) != sizeof seed)
    +	log_debug("Generating %d bytes unique pad...\n", PUSB_PAD_SIZE);
    +	if (generate_random_bytes(magic, sizeof(magic)) != 0)
     	{
    -		log_debug("/dev/random seeding failed...\n");
    -		seed = getpid() * time(NULL); /* low-entropy fallback */
    -	}
    -	if (devrandom >= 0)
    -	{
    -		close(devrandom);
    +		log_error("Failed to generate random pad data.\n");
    +		explicit_bzero(magic, sizeof(magic));
    +		return 0;
     	}
     
    -	generateRandom(magic, sizeof(magic));
    -
     	{
    -		int fd_dev = open(path_device_tmp, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600);
    +		int fd_dev = open(path_device_tmp, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0600);
     		if (fd_dev < 0 || !(f_device = fdopen(fd_dev, "w")))
     		{
     			if (fd_dev >= 0) close(fd_dev);
     			log_error("Unable to create temp device pad: %s\n", strerror(errno));
    +			explicit_bzero(magic, sizeof(magic));
     			return 0;
     		}
     	}
     	pusb_pad_protect(user, fileno(f_device));
     
     	{
    -		int fd_sys = open(path_system_tmp, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600);
    +		int fd_sys = open(path_system_tmp, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0600);
     		if (fd_sys < 0 || !(f_system = fdopen(fd_sys, "w")))
     		{
     			if (fd_sys >= 0) close(fd_sys);
     			log_error("Unable to create temp system pad: %s\n", strerror(errno));
     			fclose(f_device);
     			unlink(path_device_tmp);
    +			explicit_bzero(magic, sizeof(magic));
     			return 0;
     		}
     	}
     	pusb_pad_protect(user, fileno(f_system));
     
     	log_debug("Writing pad to the system...\n");
    -	if (fwrite(magic, sizeof(char), sizeof(magic), f_system) != sizeof(magic))
    +	if (fwrite(magic, sizeof(uint8_t), sizeof(magic), f_system) != sizeof(magic))
     	{
     		log_error("Failed to write system pad: %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;
     	}
     
     	log_debug("Writing pad to the device...\n");
    -	if (fwrite(magic, sizeof(char), sizeof(magic), f_device) != sizeof(magic))
    +	if (fwrite(magic, sizeof(uint8_t), sizeof(magic), f_device) != sizeof(magic))
     	{
     		log_error("Failed to write device pad: %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;
     	}
     
     	log_debug("Synchronizing filesystems...\n");
    +	if (fflush(f_system) != 0 || fflush(f_device) != 0)
    +	{
    +		log_error("Failed to flush pad data: %s\n", strerror(errno));
    +		fclose(f_system);
    +		fclose(f_device);
    +		unlink(path_system_tmp);
    +		unlink(path_device_tmp);
    +		return 0;
    +	}
     	fsync(fileno(f_system));
     	fsync(fileno(f_device));
     	fclose(f_system);
     	fclose(f_device);
     
    +	explicit_bzero(magic, sizeof(magic));
    +
     	if (rename(path_system_tmp, path_system) != 0)
     	{
     		log_error("Failed to install system pad: %s\n", strerror(errno));
    @@ -390,24 +423,17 @@ static int pusb_pad_update(
     	return 1;
     }
     
    -void generateRandom(char* output, int sizeBytes)
    +static int timingsafe_memcmp(const void *a, const void *b, size_t n)
     {
    -	// Based on https://www.cyrill-gremaud.ch/howto-generate-secure-random-number-on-nix/
    -	int fd, bytes_read;
    +	const uint8_t *pa = (const uint8_t *)a;
    +	const uint8_t *pb = (const uint8_t *)b;
    +	volatile uint8_t diff = 0;
    +	size_t i;
     
    -	if((fd = open("/dev/random", O_RDONLY)) == -1)
    -	{
    -		log_error("impossible to read randomness source\n");
    -		return;
    -	}
    -
    -	bytes_read = read(fd, output, sizeBytes);
    -	if (bytes_read != sizeBytes)
    -	{
    -		log_debug("read() failed (%d bytes read)\n", bytes_read);
    -	}
    +	for (i = 0; i < n; i++)
    +		diff |= pa[i] ^ pb[i];
     
    -	close(fd);
    +	return (int)diff;
     }
     
     static int pusb_pad_compare(
    @@ -418,51 +444,66 @@ static int pusb_pad_compare(
     {
     	FILE *f_device = NULL;
     	FILE *f_system = NULL;
    -	char magic_device[1024];
    -	char magic_system[1024];
    -	int retval;
    +	uint8_t magic_device[PUSB_PAD_SIZE];
    +	uint8_t magic_system[PUSB_PAD_SIZE];
    +	int retval = 0;
     	size_t bytes_read;
     
     	if (!(f_system = pusb_pad_open_system(opts, user, "r")))
     	{
    +		/* System pad absent. Peek at the device pad:
    +		 * both absent → first run, allow so update can generate both;
    +		 * device present but system gone → deleted by attacker, deny (F3). */
    +		FILE *f_dev_check = pusb_pad_open_device(opts, volume, user, "r");
    +		if (f_dev_check != NULL)
    +		{
    +			fclose(f_dev_check);
    +			log_error("System pad missing but device pad exists, denying auth.\n");
    +			return 0;
    +		}
    +		log_debug("No pads found, allowing first-time generation.\n");
     		return 1;
     	}
     
     	if (!(f_device = pusb_pad_open_device(opts, volume, user, "r")))
     	{
    +		log_error("Cannot open device pad, denying auth.\n");
     		fclose(f_system);
     		return 0;
     	}
    +
     	log_debug("Loading device pad...\n");
    -	bytes_read = fread(magic_device, sizeof(char), sizeof(magic_device), f_device);
    -	if (bytes_read != sizeof(magic_device))
    +	bytes_read = fread(magic_device, sizeof(uint8_t), PUSB_PAD_SIZE, f_device);
    +	if (bytes_read != PUSB_PAD_SIZE)
     	{
    -		log_error("Device pad is incomplete (%zu/%zu bytes).\n", bytes_read, sizeof(magic_device));
    -		fclose(f_system);
    +		log_error("Device pad is incomplete (%zu/%zu bytes).\n", bytes_read, (size_t)PUSB_PAD_SIZE);
    +		explicit_bzero(magic_device, sizeof(magic_device));
     		fclose(f_device);
    +		fclose(f_system);
     		return 0;
     	}
     
     	log_debug("Loading system pad...\n");
    -	bytes_read = fread(magic_system, sizeof(char), sizeof(magic_system), f_system);
    -	if (bytes_read != sizeof(magic_system))
    +	bytes_read = fread(magic_system, sizeof(uint8_t), PUSB_PAD_SIZE, f_system);
    +	if (bytes_read != PUSB_PAD_SIZE)
     	{
    -		log_error("System pad is incomplete (%zu/%zu bytes).\n", bytes_read, sizeof(magic_system));
    -		fclose(f_system);
    +		log_error("System pad is incomplete (%zu/%zu bytes).\n", bytes_read, (size_t)PUSB_PAD_SIZE);
    +		explicit_bzero(magic_device, sizeof(magic_device));
    +		explicit_bzero(magic_system, sizeof(magic_system));
     		fclose(f_device);
    +		fclose(f_system);
     		return 0;
     	}
     
    -	retval = memcmp(magic_system, magic_device, sizeof(magic_system));
    -	fclose(f_system);
    -	fclose(f_device);
    -
    -	if (!retval)
    -	{
    +	retval = (timingsafe_memcmp(magic_system, magic_device, PUSB_PAD_SIZE) == 0);
    +	if (retval)
     		log_debug("Pad match.\n");
    -	}
     
    -	return (retval == 0);
    +	explicit_bzero(magic_device, sizeof(magic_device));
    +	explicit_bzero(magic_system, sizeof(magic_system));
    +	fclose(f_system);
    +	fclose(f_device);
    +	return retval;
     }
     
     int pusb_pad_check(
    
  • src/pad.h+0 1 modified
    @@ -20,6 +20,5 @@
     #include <udisks/udisks.h>
     
     int pusb_pad_check(t_pusb_options *opts, UDisksClient *udisks, const char *user);
    -void generateRandom(char* output, int sizeBytes);
     
     #endif /* !PUSB_OTP_H_ */
    
  • tests/can-actually-be-used/run-tests.sh+1 0 modified
    @@ -13,4 +13,5 @@ rm -rf /home/`whoami`/.pamusb
     ./test-conf-doesnt-add-user-twice-but-adds-a-second-device.sh && \
     ./test-check-verify-created-config.sh && \
     ./test-check-superuser-filtering.sh && \
    +rm -rf /tmp/fakestick/.pamusb && \
     ./test-agent-properly-triggers.sh
    
  • tests/unit/c/pad_test.c+126 0 modified
    @@ -13,6 +13,7 @@
     #include <stddef.h>
     #include <setjmp.h>
     #include <string.h>
    +#include <stdint.h>
     #include <stdio.h>
     #include <stdlib.h>
     #include <unistd.h>
    @@ -294,6 +295,126 @@ static void test_pad_missing(void **state)
     	rmdir(sys_pad_dir);
     }
     
    +/* ── F3 regression: missing system pad must deny (not allow) ── */
    +
    +static void test_missing_system_pad_denied(void **state)
    +{
    +	(void)state;
    +	struct passwd *pw = getpwuid(getuid());
    +	assert_non_null(pw);
    +
    +	char mnt_dir[] = "/tmp/pamusb_f3sys_XXXXXX";
    +	assert_non_null(mkdtemp(mnt_dir));
    +
    +	t_pusb_options opts = {0};
    +	pusb_conf_init(&opts);
    +	snprintf(opts.device.name, sizeof(opts.device.name), "pamusb_f3test");
    +	/* Use a nonexistent system pad directory so system pad open fails */
    +	snprintf(opts.system_pad_directory, sizeof(opts.system_pad_directory),
    +	         ".pamusb_f3_unit_NONEXISTENT");
    +
    +	char sys_pad_dir[1024*5];
    +	snprintf(sys_pad_dir, sizeof(sys_pad_dir), "%s/%s", pw->pw_dir,
    +	         opts.system_pad_directory);
    +	rmdir(sys_pad_dir);
    +
    +	/* Create a valid device pad so the test isolates the missing-system-pad branch */
    +	char dev_pad_dir[512];
    +	snprintf(dev_pad_dir, sizeof(dev_pad_dir), "%s/.pamusb", mnt_dir);
    +	mkdir_p(dev_pad_dir);
    +
    +	char dev_pad_path[1024];
    +	snprintf(dev_pad_path, sizeof(dev_pad_path), "%s/%s.%s.pad",
    +	         dev_pad_dir, pw->pw_name, opts.hostname);
    +	uint8_t buf[PUSB_PAD_SIZE];
    +	memset(buf, 0xBB, sizeof(buf));
    +	write_file(dev_pad_path, buf, sizeof(buf));
    +
    +	/* missing system pad must deny (return 0) — pre-fix returned 1 */
    +	int result = pusb_pad_compare(&opts, mnt_dir, pw->pw_name);
    +	assert_int_equal(0, result);
    +
    +	unlink(dev_pad_path);
    +	rmdir(dev_pad_dir);
    +	rmdir(mnt_dir);
    +	rmdir(sys_pad_dir);
    +}
    +
    +/* ── First-run: both pads absent → allow (so update can generate them) ── */
    +
    +static void test_first_run_no_pads_allowed(void **state)
    +{
    +	(void)state;
    +	struct passwd *pw = getpwuid(getuid());
    +	assert_non_null(pw);
    +
    +	char mnt_dir[] = "/tmp/pamusb_firstrun_XXXXXX";
    +	assert_non_null(mkdtemp(mnt_dir));
    +
    +	t_pusb_options opts = {0};
    +	pusb_conf_init(&opts);
    +	snprintf(opts.device.name, sizeof(opts.device.name), "pamusb_firstrun");
    +	snprintf(opts.system_pad_directory, sizeof(opts.system_pad_directory),
    +	         ".pamusb_firstrun_unit");
    +
    +	/* Ensure system pad directory does not exist */
    +	char sys_pad_dir[1024*5];
    +	snprintf(sys_pad_dir, sizeof(sys_pad_dir), "%s/%s", pw->pw_dir,
    +	         opts.system_pad_directory);
    +	rmdir(sys_pad_dir);
    +
    +	/* Device pad directory also absent — no pads anywhere (first run) */
    +
    +	/* Must return 1 (allow) so pusb_pad_update can generate initial pads */
    +	int result = pusb_pad_compare(&opts, mnt_dir, pw->pw_name);
    +	assert_int_equal(1, result);
    +
    +	char mnt_pamusb[PATH_MAX];
    +	snprintf(mnt_pamusb, sizeof(mnt_pamusb), "%s/.pamusb", mnt_dir);
    +	rmdir(mnt_pamusb);
    +	rmdir(mnt_dir);
    +	rmdir(sys_pad_dir);
    +}
    +
    +/* ── F2 regression: generate_random_bytes fills buffer and returns 0 ── */
    +
    +static void test_generate_random_bytes_fills_buffer(void **state)
    +{
    +	(void)state;
    +	uint8_t buf[PUSB_PAD_SIZE];
    +	memset(buf, 0, sizeof(buf));
    +
    +	int ret = generate_random_bytes(buf, sizeof(buf));
    +	assert_int_equal(0, ret);
    +
    +	/* Buffer must not be all-zero (CSPRNG output with overwhelming probability) */
    +	uint8_t acc = 0;
    +	for (size_t i = 0; i < sizeof(buf); i++)
    +		acc |= buf[i];
    +	assert_int_not_equal(0, (int)acc);
    +}
    +
    +/* ── F4 regression: timingsafe_memcmp correctness ── */
    +
    +static void test_timingsafe_memcmp_equal(void **state)
    +{
    +	(void)state;
    +	uint8_t a[64], b[64];
    +	memset(a, 0x5A, sizeof(a));
    +	memset(b, 0x5A, sizeof(b));
    +	assert_int_equal(0, timingsafe_memcmp(a, b, sizeof(a)));
    +}
    +
    +static void test_timingsafe_memcmp_differ(void **state)
    +{
    +	(void)state;
    +	uint8_t a[64], b[64];
    +	memset(a, 0x5A, sizeof(a));
    +	memset(b, 0x5A, sizeof(b));
    +	b[63] = 0xA5;
    +	assert_int_not_equal(0, timingsafe_memcmp(a, b, sizeof(a)));
    +}
    +
     /* ── main ── */
     
     int main(void)
    @@ -311,6 +432,11 @@ int main(void)
     		cmocka_unit_test(test_pad_expired),
     		cmocka_unit_test(test_pad_fresh),
     		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_generate_random_bytes_fills_buffer),
    +		cmocka_unit_test(test_timingsafe_memcmp_equal),
    +		cmocka_unit_test(test_timingsafe_memcmp_differ),
     	};
     	return cmocka_run_group_tests(tests, NULL, NULL);
     }
    

Vulnerability mechanics

Root cause

"Missing symmetric verification in pusb_pad_compare() allowed authentication to succeed when only the user-side pad was present but the system-side pad was absent."

Attack vector

A local attacker with a user account on the system can delete their own `~/.pamusb/

Affected code

The vulnerability resides in `pusb_pad_compare()` in `src/pad.c` [patch_id=2749081]. The function only verified that the user-side pad (`~/.pamusb/device.pad`) could be read, but did not enforce that the system-side pad (the pad file on the USB device) was also present and readable [ref_id=1].

What the fix does

The patch [patch_id=2749081] rewrites `pusb_pad_compare()` with two-case logic: if the system pad is absent but the device pad exists, authentication is denied (return 0); if both pads are absent, authentication is allowed so that `pusb_pad_update()` can generate initial pads on first run. This closes the bypass where deleting only the user-side pad allowed authentication to succeed [ref_id=1].

Preconditions

  • authAttacker must have a local user account on the system where pam_usb is configured for OTP authentication.
  • inputThe user-side pad file (~/.pamusb/.pad) must be deletable by the attacker (it is owned by the user).

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

References

1

News mentions

0

No linked articles in our index yet.