#1110815 NULL dereference in pam_handlers.c when parsing '@include'

Package:
libpam0g
Source:
libpam0g
Description:
Pluggable Authentication Modules library
Submitter:
Alexey Panov
Date:
2025-08-11 08:27:02 UTC
Severity:
normal
#1110815#5
Date:
2025-08-11 08:19:59 UTC
From:
To:
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

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.