Files
ocserv/tests/test-pam-abort
T
Nikos Mavrogiannopoulos 0dafa7b005 tests: add regression test for pam_auth_deinit coroutine safety
Add test-pam-abort, which verifies that pam_auth_deinit() correctly
resumes a suspended PAM coroutine before calling pam_end().

The test posts a username-only HTTP request to trigger SEC_AUTH_INIT,
leaving the PAM coroutine suspended in PAM_S_WAIT_FOR_PASS while the
worker exits without sending a password.  The stale pre-auth entry is
cleaned up by the sec-mod maintenance cycle (driven by the new
sec-mod-db-cleanup-time config knob, set to 3 s in the test config).

Bug detection is provided by pam_abort_test.so, a small PAM module that
registers a pam_set_data() cleanup which calls abort() if pam_end() fires
while conv->conv() has not yet returned.  Without the fix, sec-mod would
abort and the subsequent authentication check would fail.

Relates: #741

Signed-off-by: Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
2026-06-06 08:12:13 +02:00

96 lines
3.6 KiB
Bash

#!/bin/bash
#
# Copyright (C) 2026 Nikos Mavrogiannopoulos
#
# This file is part of ocserv.
#
# ocserv is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or (at
# your option) any later version.
#
# ocserv is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GnuTLS; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Regression test for the bug fixed in commit af108817 (issue #657).
#
# When the PAM conversation coroutine is suspended in PAM_S_WAIT_FOR_PASS
# and pam_auth_deinit() is called (e.g. during maintenance cleanup of a
# stale pre-auth entry), the fix must resume the coroutine so it can
# return PAM_CONV_ERR before pam_end() is called.
#
# Bug detection:
# pam_abort_test.so registers a pam_set_data() cleanup that calls
# abort() if pam_end() fires while the conversation flag is still set.
# The flag is cleared only after the conversation function returns.
# Without the fix, pam_end() fires with the coroutine still live,
# the cleanup aborts the sec-mod process, and subsequent
# authentication fails. With the fix, the coroutine is resumed
# cleanly before pam_end() and the server survives.
#
# Trigger (PAM_S_WAIT_FOR_PASS):
# curl sends POST /auth with only "username=testuser" (no password
# field). The worker parses the username, sends SEC_AUTH_INIT to
# sec-mod which starts the PAM coroutine; the coroutine calls the
# conversation function and suspends. The worker then fails to
# extract a password from the POST body, returns 401, and exits.
# Because proc->active_sid is 0 for unauthenticated workers, main
# does not send SECM_SESSION_CLOSE to sec-mod. The stale pre-auth
# entry (in_use == 0) stays in sec-mod's database.
#
# Cleanup:
# exptime = now + cookie_timeout(0) + AUTH_SLACK_TIME(15) = now + 15 s
# sec-mod-db-cleanup-time = 3 s → maintenance fires every 3 s
# pam_auth_deinit() is called within ~18 s of the POST.
# Sleep 25 s to be safe.
srcdir=${srcdir:-.}
NO_NEED_ROOT=1
. "$(dirname "$0")"/common.sh
eval "${GETPORT}"
echo "Testing PAM cleanup when client aborts during conversation (issue #657)..."
export TEST_PAMDIR=data/pam
update_config test-pam-abort.config
launch_sr_pam_server -d 1 -f -c ${CONFIG} & PID=$!
wait_server $PID
echo ""
echo "Sending username-only POST to trigger PAM_S_WAIT_FOR_PASS..."
# POST with only username= and no password field.
# Worker sends SEC_AUTH_INIT → PAM coroutine starts, calls conv(), suspends.
# Worker fails to extract password from body → 401 → exits.
# The pre-auth entry remains in sec-mod (in_use=0, expires in 15 s).
LD_PRELOAD=libsocket_wrapper.so curl -sk \
--max-time 10 \
--data 'username=testuser' \
"https://$ADDRESS:$PORT/auth" >/dev/null 2>&1 || true
# exptime ≈ now + 15 s; maintenance fires every 3 s → cleanup within 18 s.
echo ""
echo "Waiting for maintenance cycle to call pam_auth_deinit() (~15 s)..."
sleep 25
echo ""
echo "Verifying server is still functional after abort..."
( echo "anypass" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT -q \
"$ADDRESS:$PORT" -u testuser \
--servercert=pin-sha256:xp3scfzy3rOQsv9NcOve/8YVVv+pHr4qNCXEXrNl5s8= \
--cookieonly 2>&1 ) ||
fail $PID "Server not functional after client abort during PAM conversation"
cleanup
exit 0