From afa97f427c7def0cf2ad1bb1fcda418b9af687c2 Mon Sep 17 00:00:00 2001 From: Iker Pedrosa Date: Mon, 20 Jul 2026 11:47:49 +0200 Subject: [PATCH] pam_userdb: fix password comparison timing leak Resolves: CVE-2026-54411 and RHEL-191705 Signed-off-by: Iker Pedrosa --- ....6.1-pam-userdb-password-timing-leak.patch | 172 ++++++++++++++++++ pam.spec | 9 +- 2 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 pam-1.6.1-pam-userdb-password-timing-leak.patch diff --git a/pam-1.6.1-pam-userdb-password-timing-leak.patch b/pam-1.6.1-pam-userdb-password-timing-leak.patch new file mode 100644 index 0000000..a029ee1 --- /dev/null +++ b/pam-1.6.1-pam-userdb-password-timing-leak.patch @@ -0,0 +1,172 @@ +From 30708d973b63891bf700299ce3ae0f1086398284 Mon Sep 17 00:00:00 2001 +From: vlefebvre +Date: Tue, 16 Jun 2026 16:31:29 +0200 +Subject: [PATCH] pam_userdb: fix password comparison timing leak + +* libpam/include/pam_inline.h: Include . +(pam_consttime_strcaseeq): New function that implements a constant-time, +case-insensitive string equality check. +* modules/pam_userdb/pam_userdb.c (user_lookup): Use it along with +pam_consttime_streq instead of strncmp and strncasecmp to fix +timing side-channel that leaks password prefix bytes and length +(CWE-208). + +Resolves: https://github.com/linux-pam/linux-pam/issues/992 +Co-authored-by: Dmitry V. Levin +--- + libpam/include/pam_inline.h | 20 +++++++++ + modules/pam_userdb/pam_userdb.c | 75 +++++++++++++++++++-------------- + 2 files changed, 64 insertions(+), 31 deletions(-) + +diff --git a/libpam/include/pam_inline.h b/libpam/include/pam_inline.h +index d79d6fdf2..86d131cf4 100644 +--- a/libpam/include/pam_inline.h ++++ b/libpam/include/pam_inline.h +@@ -9,6 +9,7 @@ + #define PAM_INLINE_H + + #include "pam_cc_compat.h" ++#include + #include + #include + #include +@@ -210,4 +211,36 @@ pam_read_passwords(int fd, int npass, char **passwords) + return i; + } + ++static inline int ++pam_consttime_streq(const char *userinput, const char *secret) { ++ volatile const char *u = userinput, *s = secret; ++ volatile int ret = 0; ++ ++ do { ++ ret |= *u ^ *s; ++ ++ s += !!*s; ++ } while (*u++ != '\0'); ++ ++ return ret == 0; ++} ++ ++/* ++ * Constant-time, case-insensitive string equality check. ++ * Same contract as pam_consttime_streq but uses tolower() on each byte. ++ * Runs for exactly strlen(userinput)+1 iterations regardless of secret. ++ */ ++static inline int ++pam_consttime_strcaseeq(const char *userinput, const char *secret) { ++ volatile const char *u = userinput, *s = secret; ++ volatile int ret = 0; ++ ++ do { ++ ret |= tolower((unsigned char)*u) ^ tolower((unsigned char)*s); ++ ++ s += !!*s; ++ } while (*u++ != '\0'); ++ ++ return ret == 0; ++} + #endif /* PAM_INLINE_H */ +diff --git a/modules/pam_userdb/pam_userdb.c b/modules/pam_userdb/pam_userdb.c +index bdb8553cc..b37f096cd 100644 +--- a/modules/pam_userdb/pam_userdb.c ++++ b/modules/pam_userdb/pam_userdb.c +@@ -321,15 +321,24 @@ user_lookup (pam_handle_t *pamh, const char *database, const char *cryptmode, + } else { + + /* Unknown password encryption method - +- * default to plaintext password storage ++ * default to plaintext password storage. ++ * Use constant-time comparison: strncmp/strncasecmp leak prefix bytes ++ * and the length pre-check leaks the password length (CWE-208). + */ + +- if (strlen(pass) != (size_t)data.dsize) { +- compare = 1; /* wrong password len -> wrong password */ +- } else if (ctrl & PAM_ICASE_ARG) { +- compare = strncasecmp(data.dptr, pass, data.dsize); ++ /* libdb is not guaranteed to produce null-terminated strings */ ++ char *stored = strndup(data.dptr, data.dsize); ++ if (stored == NULL) { ++ pam_syslog(pamh, LOG_CRIT, "strndup failed: data.dptr"); ++ compare = -2; + } else { +- compare = strncmp(data.dptr, pass, data.dsize); ++ if (ctrl & PAM_ICASE_ARG) { ++ compare = pam_consttime_strcaseeq(pass, stored) ? 0 : 1; ++ } else { ++ compare = pam_consttime_streq(pass, stored) ? 0 : 1; ++ } ++ pam_overwrite_string(stored); ++ free(stored); + } + + if (cryptmode && pam_str_skip_icase_prefix(cryptmode, "none") == NULL +@@ -361,36 +370,40 @@ user_lookup (pam_handle_t *pamh, const char *database, const char *cryptmode, + } + + /* now handle the key_only case */ ++ size_t ulen = strlen(user); + for (key = db_firstkey(dbm); + key.dptr != NULL; + key = db_nextkey(dbm, key)) { +- int compare; +- /* first compare the user portion (case sensitive) */ +- compare = strncmp(key.dptr, user, strlen(user)); +- if (compare == 0) { +- /* assume failure */ +- compare = -1; +- /* if we have the divider where we expect it to be... */ +- if (key.dptr[strlen(user)] == '-') { +- saw_user = 1; +- if ((size_t)key.dsize == strlen(user) + 1 + strlen(pass)) { +- if (ctrl & PAM_ICASE_ARG) { +- /* compare the password portion (case insensitive)*/ +- compare = strncasecmp(key.dptr + strlen(user) + 1, +- pass, +- strlen(pass)); +- } else { +- /* compare the password portion (case sensitive) */ +- compare = strncmp(key.dptr + strlen(user) + 1, +- pass, +- strlen(pass)); +- } +- } +- } +- if (compare == 0) { ++ /* assume failure */ ++ int compare = -1; ++ ++ /* ++ * First compare the user portion (case sensitive); ++ * user is caller-supplied, so this memcmp leaks nothing secret. ++ */ ++ if ((size_t)key.dsize > ulen && ++ key.dptr[ulen] == '-' && ++ memcmp(key.dptr, user, ulen) == 0) { ++ saw_user = 1; ++ char *stored_pass = strndup(key.dptr + ulen + 1, ++ key.dsize - ulen - 1); ++ if (stored_pass == NULL) { + db_close(dbm); +- return 0; /* match */ ++ return -2; + } ++ /* compare the password portion (case (in)sensitive) */ ++ if (ctrl & PAM_ICASE_ARG) { ++ compare = pam_consttime_strcaseeq(pass, stored_pass) ? 0 : 1; ++ } else { ++ compare = pam_consttime_streq(pass, stored_pass) ? 0 : 1; ++ } ++ pam_overwrite_string(stored_pass); ++ free(stored_pass); ++ } ++ ++ if (compare == 0) { ++ db_close(dbm); ++ return 0; /* match */ + } + } + db_close(dbm); + + diff --git a/pam.spec b/pam.spec index d3cea49..b20f8c5 100644 --- a/pam.spec +++ b/pam.spec @@ -4,7 +4,7 @@ Summary: An extensible library which provides authentication for applications Name: pam Version: 1.6.1 -Release: 10%{?dist} +Release: 11%{?dist} # The library is BSD licensed with option to relicense as GPLv2+ # - this option is redundant as the BSD license allows that anyway. # pam_timestamp and pam_loginuid modules are GPLv2+. @@ -43,6 +43,8 @@ Patch10: pam-1.6.1-pam-faillock-skip.patch Patch11: pam-1.6.1-pam-access-uid-gid-access-conf.patch # https://github.com/linux-pam/linux-pam/commit/fc927d8f1a6d81e5bcf58096871684b35b793fe2 Patch12: pam-1.6.1-pam-access-fix-group-name-match.patch +# https://github.com/linux-pam/linux-pam/commit/30708d973b63891bf700299ce3ae0f1086398284 +Patch13: pam-1.6.1-pam-userdb-password-timing-leak.patch %{load:%{SOURCE3}} @@ -145,6 +147,7 @@ cp %{SOURCE18} . %patch -P 10 -p1 -b .pam-faillock-skip %patch -P 11 -p1 -b .pam-access-uid-gid-access-conf %patch -P 12 -p1 -b .pam-access-fix-group-name-match +%patch -P 13 -p1 -b .pam-userdb-password-timing-leak autoreconf -i @@ -383,6 +386,10 @@ done %{_pam_libdir}/libpam_misc.so.%{so_ver}* %changelog +* Mon Jul 20 2026 Iker Pedrosa - 1.6.1-11 +- pam_userdb: fix password comparison timing leak. + Resolves: CVE-2026-54411 and RHEL-191705 + * Tue Apr 7 2026 Iker Pedrosa - 1.6.1-10 - pam_access: support UID and GID in access.conf Resolves: RHEL-119867