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.

Reply via email to