fix: pam_set_data error handling, setsid+fd sweep in grandchild, is_replace logic, null pamh guard

This commit is contained in:
Asger Geel Weirsøe
2026-05-06 12:05:43 +02:00
parent c24bd26ba1
commit 4b9d69ffbc

View File

@@ -10,7 +10,6 @@ pub const PAM_IGNORE: c_int = 25;
pub const PAM_AUTH_ERR: c_int = 7; pub const PAM_AUTH_ERR: c_int = 7;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub const PAM_AUTH_ERR: c_int = 9; pub const PAM_AUTH_ERR: c_int = 9;
// Fallback for CI on non-Linux non-macOS (should not occur in production)
#[cfg(not(any(target_os = "linux", target_os = "macos")))] #[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub const PAM_AUTH_ERR: c_int = 7; pub const PAM_AUTH_ERR: c_int = 7;
@@ -55,7 +54,7 @@ unsafe extern "C" fn ahfail_cleanup(
data: *mut c_void, data: *mut c_void,
error_status: c_int, error_status: c_int,
) { ) {
// Reclaim any stored path string to avoid a leak. // Reclaim any stored path string to avoid a leak (PAM guarantees this fires exactly once).
let path_override: Option<String> = if data.is_null() { let path_override: Option<String> = if data.is_null() {
None None
} else { } else {
@@ -63,8 +62,11 @@ unsafe extern "C" fn ahfail_cleanup(
}; };
if is_replace(error_status) { if is_replace(error_status) {
// Previous attempt failed, same PAM handle; a new attempt is starting. // Data replaced — a new pam_set_data call overwrote ours. Only spawn on failure;
// on success (PAM_SUCCESS | PAM_DATA_REPLACE) we do nothing.
if is_failure(error_status) {
spawn_display(path_override); spawn_display(path_override);
}
return; return;
} }
if is_failure(error_status) { if is_failure(error_status) {
@@ -83,10 +85,12 @@ pub unsafe extern "C" fn pam_sm_authenticate(
argc: c_int, argc: c_int,
argv: *const *const c_char, argv: *const *const c_char,
) -> c_int { ) -> c_int {
if pamh.is_null() { return PAM_IGNORE; }
let args = argc_argv(argc, argv); let args = argc_argv(argc, argv);
let display_path = read_display_path_arg(args); let display_path = read_display_path_arg(args);
let key = match CString::new("ahfail") { let key = match CString::new("dk.weircon.ahfail") {
Ok(k) => k, Ok(k) => k,
Err(_) => return PAM_IGNORE, Err(_) => return PAM_IGNORE,
}; };
@@ -97,7 +101,11 @@ pub unsafe extern "C" fn pam_sm_authenticate(
None => std::ptr::null_mut(), None => std::ptr::null_mut(),
}; };
pam_set_data(pamh, key.as_ptr(), path_ptr, Some(ahfail_cleanup)); let ret = pam_set_data(pamh, key.as_ptr(), path_ptr, Some(ahfail_cleanup));
if ret != PAM_SUCCESS && !path_ptr.is_null() {
// pam_set_data failed — cleanup will never fire, so free the Box ourselves.
drop(Box::from_raw(path_ptr as *mut String));
}
PAM_IGNORE PAM_IGNORE
} }
@@ -163,8 +171,12 @@ fn spawn_display(path_override: Option<String>) {
// Intermediate: fork again then exit immediately. // Intermediate: fork again then exit immediately.
let pid2: pid_t = libc::fork(); let pid2: pid_t = libc::fork();
if pid2 != 0 { libc::_exit(0); } if pid2 != 0 { libc::_exit(0); }
// Grandchild: close inherited fds, exec ahfail-display. // Grandchild: detach from PAM daemon's session, close all inherited fds, exec.
libc::close(0); libc::close(1); libc::close(2); libc::setsid();
let max_fd = libc::sysconf(libc::_SC_OPEN_MAX) as c_int;
for fd in 0..max_fd.min(4096) {
libc::close(fd);
}
let args: [*const c_char; 2] = [cpath.as_ptr(), std::ptr::null()]; let args: [*const c_char; 2] = [cpath.as_ptr(), std::ptr::null()];
libc::execv(cpath.as_ptr(), args.as_ptr()); libc::execv(cpath.as_ptr(), args.as_ptr());
libc::_exit(1); libc::_exit(1);