Package: libpam0g
Version: 1.5.2-6+deb12u1
Steps to Reproduce
1. Install pam source and dependencies from Debian bookworm
2. Compile pam_1.5.2-6+deb12u1 with ASan
3. Use @include with no argument in a custom /etc/pam.d/test_pam
4. Call pam_start() with that service name
5. Observe ASan crash (NULL deref on tok[0])
$ docker run -it --rm debian:bookworm-20250721 bash
$ echo "deb-src http://deb.debian.org/debian bookworm main" | tee
/etc/apt/sources.list
$ apt update
$ apt install build-essential devscripts nano libpam0g-dev
$ mkdir workdir && cd workdir
$ apt source libpam0g
$ ls -1
pam-1.5.2
pam_1.5.2-6+deb12u1.debian.tar.xz
pam_1.5.2-6+deb12u1.dsc
pam_1.5.2.orig.tar.xz
$ cd pam-1.5.2
$ apt build-dep -y .
$ rm -Rf .pc
$ export QUILT_PATCHES=debian/patches-applied
$ quilt push -a
# optional --enable-debug
$ ./configure CFLAGS="-g -fsanitize=address" --enable-static
--libdir=/lib/x86_64-linux-gnu --enable-isadir=/lib/security
$ make -j`nproc` -C libpam
$ make -j`nproc` -C libpam_misc
$ cat <<EOT > test.c
#include <security/pam_appl.h>
#include <security/pam_misc.h>
#include <stdio.h>
#include <stdlib.h>
static struct pam_conv conv = {misc_conv, NULL};
int main(int argc, char *argv[]) {
pam_handle_t *pamh = NULL;
int retval;
const char *user = "test";
const char *service_name = "test_pam";
retval = pam_start(service_name, user, &conv, &pamh);
if (retval != PAM_SUCCESS) {
printf("pam_start failed: %s\n", pam_strerror(pamh, retval));
pam_end(pamh, retval);
return -1;
}
printf("pam_start: %s\n", pam_strerror(pamh, retval));
retval = pam_end(pamh, retval);
printf("pam_end: %s\n", pam_strerror(pamh, retval));
return 0;
}
EOT
$ gcc -g -fsanitize=address test.c libpam/.libs/libpam.a
libpam_misc/.libs/libpam_misc.a -o test -ldl -laudit
$ export ASAN_OPTIONS=detect_leaks=0
# This will crash: @include with no argument causes NULL deref in tok[0]
$ echo "@include" | tee /etc/pam.d/test_pam
$ ./test
AddressSanitizer:DEADLYSIGNAL
=================================================================
==29568==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc
0x5594ee6ba947 bp 0x7ffe8c7b9480 sp 0x7ffe8c7b8d70 T0)
==29568==The signal is caused by a READ memory access.
==29568==Hint: address points to the zero page.
#0 0x5594ee6ba947 in _pam_parse_conf_file
/workdir/pam-1.5.2/libpam/pam_handlers.c:214
#1 0x5594ee6bbd86 in _pam_init_handlers
/workdir/pam-1.5.2/libpam/pam_handlers.c:490
#2 0x5594ee6c169d in _pam_start_internal
/workdir/pam-1.5.2/libpam/pam_start.c:144
#3 0x5594ee6c1b83 in pam_start /workdir/pam-1.5.2/libpam/pam_start.c:177
#4 0x5594ee6b66a8 in main /workdir/pam-1.5.2/test.c:11
#5 0x7fd39cd1a249 (/lib/x86_64-linux-gnu/libc.so.6+0x27249)
#6 0x7fd39cd1a304 in __libc_start_main
(/lib/x86_64-linux-gnu/libc.so.6+0x27304)
#7 0x5594ee6b6500 in _start (/workdir/pam-1.5.2/test+0x7500)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /workdir/pam-1.5.2/libpam/pam_handlers.c:214 in
_pam_parse_conf_file
==29568==ABORTING
$ echo "@include common-auth" | tee /etc/pam.d/test_pam
$ ./test
pam_start: Success
pam_end: Success
Alternatively, you can build the same test program against the
system-installed libpam without ASan, and still reproduce the crash:
gcc -g test.c -o test -lpam -lpam_misc
./test
Segmentation fault (core dumped)
While analyzing this issue, I identified the problematic patch:
debian/patches-applied/031_pam_include
"Patch to implement an @include directive for use in pam.d config files."
When @include is encountered, the code sets pam_include = 1 and jumps to
parsing_done. At that point, the next token is retrieved via:
tok = _pam_StrTok(NULL, " \n\t", &nexttok);
Later, in the if (pam_include) branch, the code attempts to dereference tok[0]
without checking if tok is NULL, which leads to a segmentation fault when
@include has no argument.
Suggested fix
Add a NULL check for tok in the pam_include condition:
diff --git a/libpam/pam_handlers.c b/libpam/pam_handlers.c
index 399690d..8726947 100644
--- a/libpam/pam_handlers.c
+++ b/libpam/pam_handlers.c
@@ -199,7 +199,7 @@ static int _pam_parse_conf_file(pam_handle_t *pamh, FILE *f
parsing_done:
tok = _pam_StrTok(NULL, " \n\t", &nexttok);
- if (pam_include) {
+ if (pam_include && tok != NULL) {
struct stat include_dir;
if (substack) {
res = _pam_add_handler(pamh, PAM_HT_SUBSTACK, other,
With this fix, if @include has no argument, the code will fall through to the
existing else branch:
} else {
/* no module name given */
D(("_pam_init_handlers: no module name supplied"));
pam_syslog(pamh, LOG_ERR,
"(%s) no module name supplied", this_service);
mod_path = NULL;
handler_type = PAM_HT_MUST_FAIL;
}
This avoids the crash and handles the error gracefully, as intended.