Fix CVE-2026-54411: pam_userdb password comparison timing leak

Backport fix for CVE-2026-54411 to pam 1.3.1 on RHEL 8.
The pam_userdb module used strncmp/strncasecmp for password
comparison, which is vulnerable to timing side-channel attacks
that can leak password prefix bytes and length (CWE-208).

A new patch (pam-1.3.1-CVE-2026-54411.patch) adds constant-time
string comparison helpers (pam_consttime_streq and
pam_consttime_strcaseeq) to pam_inline.h and rewrites
pam_userdb.c to use them. The patch combines two upstream
commits (c11ccdfa and 30708d97) adapted for the RHEL 8 codebase.

CVE: CVE-2026-54411
Upstream patches:
 - c11ccdfad1.patch
 - https://src.fedoraproject.org/rpms/pam/raw/main/f/pam-1.7.2-pam-userdb-fix-password-leak.patch
Resolves: RHEL-191699

This commit was backported by Ymir, a Red Hat Enterprise Linux software maintenance AI agent.

Assisted-by: Ymir
This commit is contained in:
RHEL Packaging Agent 2026-07-12 09:20:48 +00:00 committed by Iker Pedrosa
parent 8fb633cfad
commit 32839ce5e4
2 changed files with 213 additions and 1 deletions

View File

@ -0,0 +1,204 @@
From 2cde1639a00d9c46ed5677a65aa3ef4e7ed4fede Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= <cgzones@googlemail.com>
Date: Sat, 20 Jan 2024 14:03:51 +0100
Subject: [PATCH 1/2] libpam: add helper to compare strings in constant time
Add a helper function to compare two strings for equality, that performs
the same amount of operations based on the first argument, regardless of
the length of the second argument, or the position of the first
difference.
This can be used as defense-in-depth mitigation against timing attacks
of password comparisons.
---
libpam/include/pam_inline.h | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/libpam/include/pam_inline.h b/libpam/include/pam_inline.h
index dd93394..2450d50 100644
--- a/libpam/include/pam_inline.h
+++ b/libpam/include/pam_inline.h
@@ -100,4 +100,18 @@ pam_snprintf(char *str, size_t size, const char *fmt, ...)
pam_snprintf((str_), sizeof(str_) + PAM_MUST_BE_ARRAY(str_), (fmt_), \
##__VA_ARGS__)
+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;
+}
+
#endif /* PAM_INLINE_H */
From f22f663d1fdf3ab53a9f569190a0070a130dd7d5 Mon Sep 17 00:00:00 2001
From: vlefebvre <valentin.lefebvre@suse.com>
Date: Tue, 16 Jun 2026 16:31:29 +0200
Subject: [PATCH 2/2] pam_userdb: fix password comparison timing leak
* libpam/include/pam_inline.h: Include <ctype.h>.
(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 <ldv@strace.io>
---
libpam/include/pam_inline.h | 20 +++++++++
modules/pam_userdb/pam_userdb.c | 76 +++++++++++++++++++--------------
2 files changed, 65 insertions(+), 31 deletions(-)
diff --git a/libpam/include/pam_inline.h b/libpam/include/pam_inline.h
index 2450d50..b6aa0cc 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 <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
@@ -114,4 +115,23 @@ pam_consttime_streq(const char *userinput, const char *secret) {
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 8794cf6..7d425c3 100644
--- a/modules/pam_userdb/pam_userdb.c
+++ b/modules/pam_userdb/pam_userdb.c
@@ -48,6 +48,7 @@
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#include <security/_pam_macros.h>
+#include "pam_inline.h"
/*
* Conversation function to obtain the user's password
@@ -259,15 +260,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(stored);
+ free(stored);
}
if (cryptmode && strncasecmp(cryptmode, "none", 4)
@@ -299,36 +309,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 = dbm_firstkey(dbm);
key.dptr != NULL;
key = dbm_nextkey(dbm)) {
- 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) {
dbm_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(stored_pass);
+ free(stored_pass);
+ }
+
+ if (compare == 0) {
+ dbm_close(dbm);
+ return 0; /* match */
}
}
dbm_close(dbm);

View File

@ -3,7 +3,7 @@
Summary: An extensible library which provides authentication for applications
Name: pam
Version: 1.3.1
Release: 39%{?dist}
Release: 40%{?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, pam_loginuid, and pam_console modules are GPLv2+.
@ -131,6 +131,9 @@ Patch77: pam-1.3.1-pam-namespace-rebase.patch
# https://github.com/linux-pam/linux-pam/commit/51a06bc8cc2278c6e81c9c08a9381c9eb0d2de96
# https://github.com/linux-pam/linux-pam/commit/470b5bdd8fd29d6b35e3a80f9a57bdd4b2438200
Patch78: pam-1.3.1-pam-unix-blank-expiration.patch
# https://github.com/linux-pam/linux-pam/commit/c11ccdfad1596199713f75a61f34672f7529ab73
# https://github.com/linux-pam/linux-pam/commit/30708d973b63891bf700299ce3ae0f1086398284
Patch79: pam-1.3.1-CVE-2026-54411.patch
%define _pamlibdir %{_libdir}
%define _moduledir %{_libdir}/security
@ -257,6 +260,7 @@ cp %{SOURCE18} .
%patch76 -p1 -b .pam-inline-pam-asprintf
%patch77 -p1 -b .pam-namespace-rebase
%patch78 -p1 -b .pam-unix-blank-expiration
%patch79 -p1 -b .CVE-2026-54411
autoreconf -i
@ -510,6 +514,10 @@ done
%doc doc/specs/rfc86.0.txt
%changelog
* Sun Jul 12 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 1.3.1-40
- pam_userdb: fix password comparison timing leak.
Resolves: CVE-2026-54411 and RHEL-191699
* Fri Nov 14 2025 Iker Pedrosa <ipedrosa@redhat.com> - 1.3.1-39
- pam_unix: sync expiry checks with shadow.
Resolves: RHEL-70476