Files
gtk-ahfail/crates/ahfail-pam/src/lib.rs
Asger Geel Weirsøe 5817074f1a
Some checks failed
Test / test (push) Failing after 4m25s
Release / release (push) Failing after 6m37s
release: v0.9.0 pre-release — X11 PAM integration, shadow suppression, macOS cfg guards
- PAM module now correctly fires on failed attempts (must precede auth includes)
- ahfail-display: sprite-sized window replaces full-screen overlay
- ahfail-display: XGrabKeyboard unlock detection replaces process scanning
- ahfail-display: _COMPTON_SHADOW=0 suppresses picom shadow without config changes
- ahfail-display: flock single-instance guard prevents stacking on rapid failures
- X11-specific code guarded behind #[cfg(target_os = "linux")] for macOS builds
- Removed debug logging (dlog/plog) from display and PAM binaries
- README: PAM ordering requirement, supported locker table, roadmap section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 22:01:02 +02:00

147 lines
4.5 KiB
Rust

use libc::{c_char, c_int, c_void, pid_t};
use std::ffi::CString;
pub const PAM_SUCCESS: c_int = 0;
pub const PAM_IGNORE: c_int = 25;
#[cfg(target_os = "linux")]
pub const PAM_DATA_REPLACE: c_int = 0x20000000u32 as i32;
#[cfg(target_os = "macos")]
pub const PAM_DATA_REPLACE: c_int = 0x00000002;
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub const PAM_DATA_REPLACE: c_int = 0x20000000u32 as i32;
const DEFAULT_PATH: &str = concat!(env!("AHFAIL_INSTALL_DIR"), "/ahfail-display");
pub fn default_display_path() -> &'static str { DEFAULT_PATH }
#[repr(C)]
pub struct PamHandle { _private: [u8; 0] }
type CleanupFn = unsafe extern "C" fn(*mut PamHandle, *mut c_void, c_int);
extern "C" {
fn pam_set_data(
pamh: *mut PamHandle,
name: *const c_char,
data: *mut c_void,
cleanup: Option<CleanupFn>,
) -> c_int;
}
unsafe extern "C" fn ahfail_cleanup(
_pamh: *mut PamHandle,
data: *mut c_void,
_error_status: c_int,
) {
if !data.is_null() {
drop(Box::from_raw(data as *mut String));
}
}
#[no_mangle]
pub unsafe extern "C" fn pam_sm_authenticate(
pamh: *mut PamHandle,
_flags: c_int,
argc: c_int,
argv: *const *const c_char,
) -> c_int {
if pamh.is_null() { return PAM_IGNORE; }
let args = argc_argv(argc, argv);
let display_path = read_display_path_arg(args);
// /proc/self/comm is the name of the current process (the locker that loaded us).
let locker_name = std::fs::read_to_string("/proc/self/comm")
.map(|s| s.trim().to_string())
.unwrap_or_default();
spawn_display(display_path.clone(), &locker_name);
let key = match CString::new("dk.weircon.ahfail") {
Ok(k) => k,
Err(_) => return PAM_IGNORE,
};
let path_ptr: *mut c_void = match display_path {
Some(p) => Box::into_raw(Box::new(p)) as *mut c_void,
None => std::ptr::null_mut(),
};
let ret = pam_set_data(pamh, key.as_ptr(), path_ptr, Some(ahfail_cleanup));
if ret != PAM_SUCCESS && !path_ptr.is_null() {
drop(Box::from_raw(path_ptr as *mut String));
}
PAM_IGNORE
}
#[no_mangle]
pub unsafe extern "C" fn pam_sm_setcred(
_pamh: *mut PamHandle, _flags: c_int, _argc: c_int, _argv: *const *const c_char,
) -> c_int { PAM_IGNORE }
#[no_mangle]
pub unsafe extern "C" fn pam_sm_acct_mgmt(
_pamh: *mut PamHandle, _flags: c_int, _argc: c_int, _argv: *const *const c_char,
) -> c_int { PAM_IGNORE }
#[no_mangle]
pub unsafe extern "C" fn pam_sm_open_session(
_pamh: *mut PamHandle, _flags: c_int, _argc: c_int, _argv: *const *const c_char,
) -> c_int { PAM_IGNORE }
#[no_mangle]
pub unsafe extern "C" fn pam_sm_close_session(
_pamh: *mut PamHandle, _flags: c_int, _argc: c_int, _argv: *const *const c_char,
) -> c_int { PAM_IGNORE }
#[no_mangle]
pub unsafe extern "C" fn pam_sm_chauthtok(
_pamh: *mut PamHandle, _flags: c_int, _argc: c_int, _argv: *const *const c_char,
) -> c_int { PAM_IGNORE }
unsafe fn argc_argv<'a>(argc: c_int, argv: *const *const c_char) -> &'a [*const c_char] {
if argv.is_null() || argc <= 0 { return &[]; }
std::slice::from_raw_parts(argv, argc as usize)
}
fn read_display_path_arg(args: &[*const c_char]) -> Option<String> {
let prefix = b"display_path=";
for &arg in args {
if arg.is_null() { continue; }
let s = unsafe { std::ffi::CStr::from_ptr(arg) }.to_bytes();
if s.starts_with(prefix) {
return std::str::from_utf8(&s[prefix.len()..]).ok().map(|s| s.to_string());
}
}
None
}
fn spawn_display(path_override: Option<String>, locker_name: &str) {
let path = path_override.unwrap_or_else(|| DEFAULT_PATH.to_string());
let cpath = match CString::new(path.as_str()) {
Ok(p) => p,
Err(_) => return,
};
let name_arg = format!("--locker-name={}", locker_name);
let cname_arg = match CString::new(name_arg.as_str()) {
Ok(p) => p,
Err(_) => return,
};
unsafe {
let pid: pid_t = libc::fork();
if pid < 0 { return; }
if pid > 0 {
libc::waitpid(pid, std::ptr::null_mut(), 0);
return;
}
let pid2: pid_t = libc::fork();
if pid2 != 0 { libc::_exit(0); }
// Grandchild: detach and exec.
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; 3] = [cpath.as_ptr(), cname_arg.as_ptr(), std::ptr::null()];
libc::execv(cpath.as_ptr(), args.as_ptr());
libc::_exit(1);
}
}