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>
This commit is contained in:
@@ -1,18 +1,9 @@
|
||||
use libc::{c_char, c_int, c_void, pid_t};
|
||||
use std::ffi::CString;
|
||||
|
||||
// --- PAM constants (platform-specific) ---
|
||||
|
||||
pub const PAM_SUCCESS: c_int = 0;
|
||||
pub const PAM_IGNORE: c_int = 25;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const PAM_AUTH_ERR: c_int = 7;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const PAM_AUTH_ERR: c_int = 9;
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
pub const PAM_AUTH_ERR: c_int = 7;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const PAM_DATA_REPLACE: c_int = 0x20000000u32 as i32;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -20,19 +11,10 @@ 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;
|
||||
|
||||
// Default install path for ahfail-display — baked in at build time via AHFAIL_INSTALL_DIR,
|
||||
// which build.rs derives from AHFAIL_LIBDIR (passed by Meson or the install script).
|
||||
// This is correct on multiarch Linux and on both Intel and Apple Silicon macOS.
|
||||
const DEFAULT_PATH: &str = concat!(env!("AHFAIL_INSTALL_DIR"), "/ahfail-display");
|
||||
|
||||
pub fn default_display_path() -> &'static str { DEFAULT_PATH }
|
||||
|
||||
pub fn is_failure(s: c_int) -> bool { s & !PAM_DATA_REPLACE == PAM_AUTH_ERR }
|
||||
pub fn is_success(s: c_int) -> bool { s & !PAM_DATA_REPLACE == PAM_SUCCESS }
|
||||
pub fn is_replace(s: c_int) -> bool { s & PAM_DATA_REPLACE != 0 }
|
||||
|
||||
// --- PAM opaque handle ---
|
||||
|
||||
#[repr(C)]
|
||||
pub struct PamHandle { _private: [u8; 0] }
|
||||
|
||||
@@ -47,36 +29,16 @@ extern "C" {
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
// --- Cleanup: fires on pam_end() or when data is replaced ---
|
||||
unsafe extern "C" fn ahfail_cleanup(
|
||||
_pamh: *mut PamHandle,
|
||||
data: *mut c_void,
|
||||
error_status: c_int,
|
||||
_error_status: c_int,
|
||||
) {
|
||||
// Reclaim any stored path string to avoid a leak (PAM guarantees this fires exactly once).
|
||||
let path_override: Option<String> = if data.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(*Box::from_raw(data as *mut String))
|
||||
};
|
||||
|
||||
if is_replace(error_status) {
|
||||
// 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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if is_failure(error_status) {
|
||||
spawn_display(path_override);
|
||||
} else if is_success(error_status) {
|
||||
kill_display();
|
||||
if !data.is_null() {
|
||||
drop(Box::from_raw(data as *mut String));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Exported PAM module entry points ---
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn pam_sm_authenticate(
|
||||
pamh: *mut PamHandle,
|
||||
@@ -89,20 +51,23 @@ pub unsafe extern "C" fn pam_sm_authenticate(
|
||||
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,
|
||||
};
|
||||
|
||||
// Store optional path override as heap data for the cleanup function.
|
||||
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() {
|
||||
// pam_set_data failed — cleanup will never fire, so free the Box ourselves.
|
||||
drop(Box::from_raw(path_ptr as *mut String));
|
||||
}
|
||||
PAM_IGNORE
|
||||
@@ -133,8 +98,6 @@ 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 }
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
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)
|
||||
@@ -152,40 +115,32 @@ fn read_display_path_arg(args: &[*const c_char]) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn spawn_display(path_override: Option<String>) {
|
||||
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,
|
||||
};
|
||||
// Double-fork: grandchild adopted by init, PAM stack not blocked.
|
||||
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 {
|
||||
// Parent: wait for intermediate to exit immediately.
|
||||
libc::waitpid(pid, std::ptr::null_mut(), 0);
|
||||
return;
|
||||
}
|
||||
// Intermediate: fork again then exit immediately.
|
||||
let pid2: pid_t = libc::fork();
|
||||
if pid2 != 0 { libc::_exit(0); }
|
||||
// Grandchild: detach from PAM daemon's session, close all inherited fds, exec.
|
||||
// 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; 2] = [cpath.as_ptr(), std::ptr::null()];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn kill_display() {
|
||||
// SIGTERM all ahfail-display processes owned by the current user.
|
||||
let uid = unsafe { libc::getuid() }.to_string();
|
||||
let _ = std::process::Command::new("pkill")
|
||||
.args(["-SIGTERM", "-u", &uid, "ahfail-display"])
|
||||
.spawn();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user