diff --git a/src/bin/souveraine-sessiond.rs b/src/bin/souveraine-sessiond.rs index da3f78c..d39e6a2 100644 --- a/src/bin/souveraine-sessiond.rs +++ b/src/bin/souveraine-sessiond.rs @@ -27,18 +27,12 @@ fn main() -> anyhow::Result<()> { ) .init(); - let mut pam_service = sessiond::protocol::DEFAULT_PAM_SERVICE.to_string(); let mut initial_lock = true; let mut socket: Option = None; let mut args = std::env::args().skip(1); while let Some(arg) = args.next() { match arg.as_str() { - "--pam-service" => { - pam_service = args - .next() - .ok_or_else(|| anyhow::anyhow!("--pam-service requires a name"))?; - } "--socket" => { socket = Some(PathBuf::from(args.next().ok_or_else(|| { anyhow::anyhow!("--socket requires a path") @@ -47,8 +41,7 @@ fn main() -> anyhow::Result<()> { "--no-initial-lock" => initial_lock = false, "--help" | "-h" => { println!( - "souveraine-sessiond [--pam-service {}] [--socket $XDG_RUNTIME_DIR/{}] [--no-initial-lock]", - sessiond::protocol::DEFAULT_PAM_SERVICE, + "souveraine-sessiond [--socket $XDG_RUNTIME_DIR/{}] [--no-initial-lock]", sessiond::protocol::SOCKET_RELPATH, ); return Ok(()); @@ -61,5 +54,5 @@ fn main() -> anyhow::Result<()> { Some(p) => p, None => sessiond::server::socket_path()?, }; - sessiond::server::run(pam_service, initial_lock, &socket) + sessiond::server::run(initial_lock, &socket) } diff --git a/src/sessiond/auth.rs b/src/sessiond/auth.rs deleted file mode 100644 index 15cdf19..0000000 --- a/src/sessiond/auth.rs +++ /dev/null @@ -1,183 +0,0 @@ -//! Minimal PAM client for the fallback unlock surface. -//! -//! Hand-rolled FFI against libpam rather than a wrapper crate: the daemon -//! asks exactly one question ("does this PIN authenticate this user") and a -//! dependency is a bigger liability than 100 lines of well-understood -//! unsafe. pam_unix answers via the setuid unix_chkpwd helper, so this works -//! from an unprivileged session process — the same mechanism hyprlock and -//! the shell's PamContext rely on. -//! -//! Doctrine: auth goes through PAM, always (SESSION-AUTHORITY-DOCTRINE §4). -//! This module never inspects credentials itself; it carries the PIN to the -//! stack configured for the service and reports the verdict. -//! -//! The expected PAM service config is documented in -//! docs/pam-souveraine-sessiond.md. The file `/etc/pam.d/souveraine-sessiond` -//! is shipped as root-owned system config by the OS overlay, never by this -//! crate. Without it, pam_start fails and the lock session does not start. - -use std::ffi::{c_char, c_int, c_void, CStr, CString}; - -const PAM_SUCCESS: c_int = 0; -const PAM_CONV_ERR: c_int = 19; - -const PAM_PROMPT_ECHO_OFF: c_int = 1; -const PAM_PROMPT_ECHO_ON: c_int = 2; -const PAM_ERROR_MSG: c_int = 3; -const PAM_TEXT_INFO: c_int = 4; - -#[repr(C)] -struct PamMessage { - msg_style: c_int, - msg: *const c_char, -} - -#[repr(C)] -struct PamResponse { - resp: *mut c_char, - resp_retcode: c_int, -} - -#[repr(C)] -struct PamConv { - conv: unsafe extern "C" fn( - num_msg: c_int, - msg: *mut *const PamMessage, - resp: *mut *mut PamResponse, - appdata_ptr: *mut c_void, - ) -> c_int, - appdata_ptr: *mut c_void, -} - -#[link(name = "pam")] -extern "C" { - fn pam_start( - service: *const c_char, - user: *const c_char, - conv: *const PamConv, - handle: *mut *mut c_void, - ) -> c_int; - fn pam_authenticate(handle: *mut c_void, flags: c_int) -> c_int; - fn pam_end(handle: *mut c_void, status: c_int) -> c_int; - fn pam_strerror(handle: *mut c_void, errnum: c_int) -> *const c_char; -} - -/// The conversation only ever answers prompts with the PIN. Info/error -/// messages are acknowledged and dropped — the fallback surface has nowhere -/// to render a module's prose and must not block on it. -unsafe extern "C" fn conv_fn( - num_msg: c_int, - msg: *mut *const PamMessage, - resp: *mut *mut PamResponse, - appdata_ptr: *mut c_void, -) -> c_int { - if num_msg <= 0 || msg.is_null() || resp.is_null() || appdata_ptr.is_null() { - return PAM_CONV_ERR; - } - let pin = &*(appdata_ptr as *const CString); - - // Linux-PAM passes an array of message pointers. Responses are freed by - // the module with free(), so they must come from malloc/strdup. - let responses = - libc::calloc(num_msg as usize, std::mem::size_of::()) as *mut PamResponse; - if responses.is_null() { - return PAM_CONV_ERR; - } - - // Linux-PAM convention: *msg points at a contiguous array of messages - // (modules there prompt one message at a time anyway). - for i in 0..num_msg as usize { - let m = (*msg).add(i); - if m.is_null() { - libc::free(responses as *mut c_void); - return PAM_CONV_ERR; - } - let slot = &mut *responses.add(i); - match (*m).msg_style { - PAM_PROMPT_ECHO_OFF | PAM_PROMPT_ECHO_ON => { - slot.resp = libc::strdup(pin.as_ptr()); - if slot.resp.is_null() { - libc::free(responses as *mut c_void); - return PAM_CONV_ERR; - } - } - PAM_ERROR_MSG | PAM_TEXT_INFO => { - slot.resp = std::ptr::null_mut(); - } - _ => { - libc::free(responses as *mut c_void); - return PAM_CONV_ERR; - } - } - slot.resp_retcode = 0; - } - - *resp = responses; - PAM_SUCCESS -} - -/// Run one authenticate conversation. Blocking (pam_unix sleeps ~2s on a -/// wrong answer); callers own threading. Returns Ok(()) on success and the -/// stack's complaint otherwise. -pub fn authenticate(service: &str, user: &str, pin: &str) -> Result<(), String> { - let service = CString::new(service).map_err(|_| "service contains NUL".to_string())?; - let user_c = CString::new(user).map_err(|_| "user contains NUL".to_string())?; - let pin = CString::new(pin).map_err(|_| "pin contains NUL".to_string())?; - - // Boxed so the conversation callback has a stable pointer for the whole - // transaction; dropped after pam_end. - let pin = Box::new(pin); - let conv = PamConv { - conv: conv_fn, - appdata_ptr: &*pin as *const CString as *mut c_void, - }; - - let mut handle: *mut c_void = std::ptr::null_mut(); - // SAFETY: all pointers live across the calls; handle is only used - // between a successful pam_start and pam_end. - unsafe { - let rc = pam_start(service.as_ptr(), user_c.as_ptr(), &conv, &mut handle); - if rc != PAM_SUCCESS { - return Err(format!("pam_start failed (code {rc})")); - } - let rc = pam_authenticate(handle, 0); - let verdict = if rc == PAM_SUCCESS { - Ok(()) - } else { - let msg = pam_strerror(handle, rc); - Err(if msg.is_null() { - format!("authentication failed (code {rc})") - } else { - CStr::from_ptr(msg).to_string_lossy().into_owned() - }) - }; - pam_end(handle, rc); - verdict - } -} - -/// The user this session belongs to, from the process's own identity — -/// sessiond authenticates the seat owner, never a name a caller supplies. -pub fn session_user() -> Result { - // SAFETY: getpwuid_r with a stack buffer; result pointer checked. - unsafe { - let uid = libc::getuid(); - let mut pwd: libc::passwd = std::mem::zeroed(); - let mut buf = [0u8; 1024]; - let mut result: *mut libc::passwd = std::ptr::null_mut(); - // libc's `c_char` is i8 on some targets and u8 on others (aarch64), - // so the buffer is `u8` and we cast through `c_char` to satisfy - // whichever signature the target's libc crate exposes. - let rc = libc::getpwuid_r( - uid, - &mut pwd, - buf.as_mut_ptr() as *mut libc::c_char, - buf.len(), - &mut result, - ); - if rc != 0 || result.is_null() { - return Err(format!("getpwuid_r failed for uid {uid}")); - } - Ok(CStr::from_ptr(pwd.pw_name).to_string_lossy().into_owned()) - } -} diff --git a/src/sessiond/draw.rs b/src/sessiond/draw.rs index a0349c2..7f9cadb 100644 --- a/src/sessiond/draw.rs +++ b/src/sessiond/draw.rs @@ -1,260 +1,46 @@ -//! Software renderer for the fallback lock surface. +//! Software renderer for sessiond's holding surface. //! -//! Deliberately spartan: a dark field, a row of PIN dots, a 3x4 keypad, -//! digits from a 5x7 bitmap font. This surface exists for the seconds -//! before the shell takes over at boot and for the case where the shell is -//! a corpse — it competes on availability, not looks. The rich lockscreen -//! (swipe, widgets) stays in the shell. +//! A dark field. That is the whole surface, deliberately. +//! +//! Until 2026-07-29 this drew a PIN pad — dots, a 3x4 keypad, a 5x7 digit +//! font, a failed-attempt counter — because sessiond could authenticate and +//! unlock. It cannot any more, and it should not: the protocol has a lock +//! directive and no unlock directive, so a successful PAM here opened the +//! compositor's lock and had no way to tell the shell, which then re-locked on +//! its next registration. A keypad that takes input and cannot finish the job +//! is worse than no keypad, so the keypad is gone rather than left inert. +//! +//! Nothing replaced it. A glyph on a locked screen with no text renderer is +//! not legible — the honest place for "the shell is not running and this +//! session is held locked" is the journal, where it can be read, searched and +//! correlated. `lock.rs` says it there. //! //! All drawing targets a raw ARGB8888 buffer (little-endian: B,G,R,A). -#![allow(dead_code)] - +/// The field. Matches the shell lockscreen's backdrop closely enough that a +/// handoff in either direction is not a visible flash. pub const BG: u32 = 0xFF0E0E12; -pub const KEY_FILL: u32 = 0xFF1D1D26; -pub const KEY_PRESSED: u32 = 0xFF3C3C4C; -pub const FG: u32 = 0xFFE6E6F0; -pub const DOT_EMPTY: u32 = 0xFF44444F; -pub const FAIL: u32 = 0xFFC63A4A; -pub const BUSY: u32 = 0xFF8A8A99; -/// Keypad cells in layout order: 1-9, backspace, 0, enter. +/// What the surface is currently expressing. One variant today; kept as an +/// enum because the surface still has states worth distinguishing later +/// (holding for a shell that is starting, vs holding over a dead one) and a +/// bool would have to be renamed to say so. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Key { - Digit(u8), - Backspace, - Enter, +pub enum Mood { + /// The session is locked and this daemon is holding it. + Holding, } #[derive(Debug, Clone, Copy)] -pub struct Rect { - pub x: i32, - pub y: i32, - pub w: i32, - pub h: i32, -} - -impl Rect { - pub fn contains(&self, px: f64, py: f64) -> bool { - px >= self.x as f64 - && py >= self.y as f64 - && px < (self.x + self.w) as f64 - && py < (self.y + self.h) as f64 - } -} - -/// 5x7 glyphs, one bit per pixel, top row first. -fn glyph(key: Key) -> [u8; 7] { - match key { - Key::Digit(0) => [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110], - Key::Digit(1) => [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110], - Key::Digit(2) => [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111], - Key::Digit(3) => [0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110], - Key::Digit(4) => [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010], - Key::Digit(5) => [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110], - Key::Digit(6) => [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110], - Key::Digit(7) => [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000], - Key::Digit(8) => [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110], - Key::Digit(9) => [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100], - Key::Digit(_) => [0; 7], - // "<" — take one off - Key::Backspace => [0b00010, 0b00100, 0b01000, 0b10000, 0b01000, 0b00100, 0b00010], - // check mark — submit - Key::Enter => [0b00000, 0b00001, 0b00010, 0b00100, 0b10100, 0b01000, 0b00000], - } -} - -/// Visual feedback state, decided by the caller's state machine. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Mood { - Entering, - Checking, - Failed, -} - pub struct Scene { - pub pin_len: usize, pub mood: Mood, - pub pressed: Option, - /// Boot-handoff face: render nothing but the dark field. The keypad - /// appears on first input, so a normal boot (shell takes over in - /// seconds) never flashes a second PIN style at the user. - pub quiet: bool, - /// Number of failed authentication attempts. Displayed below the PIN - /// dots so the user knows how many tries they've had. Advisory only — - /// the actual lockout policy lives in PAM. - pub failed_attempts: u32, } -/// The keypad geometry for a surface of w x h. Pure function of size so -/// input hit-testing and drawing can never disagree. -pub fn keypad_layout(w: i32, h: i32) -> Vec<(Rect, Key)> { - let keys = [ - Key::Digit(1), - Key::Digit(2), - Key::Digit(3), - Key::Digit(4), - Key::Digit(5), - Key::Digit(6), - Key::Digit(7), - Key::Digit(8), - Key::Digit(9), - Key::Backspace, - Key::Digit(0), - Key::Enter, - ]; - // Grid occupies the lower ~55% of the surface, centered, capped width. - let grid_w = (w * 8 / 10).min(520); - let grid_h = h * 55 / 100; - let x0 = (w - grid_w) / 2; - let y0 = h - grid_h - h / 20; - let cell_w = grid_w / 3; - let cell_h = grid_h / 4; - let pad = (cell_w / 10).max(4); - - keys.iter() - .enumerate() - .map(|(i, &k)| { - let col = (i % 3) as i32; - let row = (i / 3) as i32; - ( - Rect { - x: x0 + col * cell_w + pad, - y: y0 + row * cell_h + pad, - w: cell_w - 2 * pad, - h: cell_h - 2 * pad, - }, - k, - ) - }) - .collect() -} - -pub fn hit_test(w: i32, h: i32, px: f64, py: f64) -> Option { - keypad_layout(w, h) - .into_iter() - .find(|(r, _)| r.contains(px, py)) - .map(|(_, k)| k) -} - -fn fill_rect(buf: &mut [u32], w: i32, h: i32, r: Rect, color: u32) { - let x0 = r.x.max(0); - let y0 = r.y.max(0); - let x1 = (r.x + r.w).min(w); - let y1 = (r.y + r.h).min(h); - for y in y0..y1 { - let row = (y * w) as usize; - for x in x0..x1 { - buf[row + x as usize] = color; - } - } -} - -fn fill_circle(buf: &mut [u32], w: i32, h: i32, cx: i32, cy: i32, radius: i32, color: u32) { - let r2 = radius * radius; - for y in (cy - radius).max(0)..(cy + radius + 1).min(h) { - for x in (cx - radius).max(0)..(cx + radius + 1).min(w) { - let dx = x - cx; - let dy = y - cy; - if dx * dx + dy * dy <= r2 { - buf[(y * w + x) as usize] = color; - } - } - } -} - -fn draw_glyph(buf: &mut [u32], w: i32, h: i32, key: Key, cell: Rect, color: u32) { - let g = glyph(key); - let scale = (cell.h / 12).max(2); - let gw = 5 * scale; - let gh = 7 * scale; - let ox = cell.x + (cell.w - gw) / 2; - let oy = cell.y + (cell.h - gh) / 2; - for (row, bits) in g.iter().enumerate() { - for col in 0..5 { - if bits & (1 << (4 - col)) != 0 { - fill_rect( - buf, - w, - h, - Rect { - x: ox + col as i32 * scale, - y: oy + row as i32 * scale, - w: scale, - h: scale, - }, - color, - ); - } - } - } -} - -/// Render the whole scene into an ARGB8888 buffer of exactly w*h pixels. pub fn render(buf: &mut [u32], w: i32, h: i32, scene: &Scene) { debug_assert_eq!(buf.len(), (w * h) as usize); + let _ = scene; + let _ = (w, h); buf.fill(BG); - if scene.quiet { - return; - } - - // PIN dots, centered in the upper region above the keypad. - let dot_color = match scene.mood { - Mood::Entering => FG, - Mood::Checking => BUSY, - Mood::Failed => FAIL, - }; - let shown = scene.pin_len.min(12).max(0); - let slots = shown.max(4); // always draw at least 4 positions - let radius = (w / 60).clamp(5, 12); - let gap = radius * 4; - let total = (slots as i32 - 1) * gap; - let cy = h / 5; - let cx0 = w / 2 - total / 2; - for i in 0..slots { - let cx = cx0 + i as i32 * gap; - if i < shown { - fill_circle(buf, w, h, cx, cy, radius, dot_color); - } else { - fill_circle(buf, w, h, cx, cy, radius, DOT_EMPTY); - fill_circle(buf, w, h, cx, cy, radius - 2, BG); - } - } - - // Failed attempt counter: rendered as a red number below the PIN dots. - // Only visible after at least one failed attempt. Uses the same digit - // glyphs as the keypad. - if scene.failed_attempts > 0 { - let counter_y = cy + radius * 3; - let digits: Vec = scene.failed_attempts - .to_string() - .bytes() - .map(|b| b - b'0') - .collect(); - let digit_w = (w / 20).clamp(12, 24); - let digit_h = digit_w * 7 / 5; - let counter_total = digits.len() as i32 * digit_w; - let counter_x0 = w / 2 - counter_total / 2; - for (i, &d) in digits.iter().enumerate() { - let cell = Rect { - x: counter_x0 + i as i32 * digit_w, - y: counter_y, - w: digit_w, - h: digit_h, - }; - draw_glyph(buf, w, h, Key::Digit(d), cell, FAIL); - } - } - - for (rect, key) in keypad_layout(w, h) { - let fill = if scene.pressed == Some(key) { - KEY_PRESSED - } else { - KEY_FILL - }; - fill_rect(buf, w, h, rect, fill); - draw_glyph(buf, w, h, key, rect, FG); - } } #[cfg(test)] @@ -262,65 +48,20 @@ mod tests { use super::*; #[test] - fn layout_and_hit_test_agree() { - let (w, h) = (1080, 2160); - for (rect, key) in keypad_layout(w, h) { - let cx = rect.x as f64 + rect.w as f64 / 2.0; - let cy = rect.y as f64 + rect.h as f64 / 2.0; - assert_eq!(hit_test(w, h, cx, cy), Some(key)); - } - assert_eq!(hit_test(w, h, 5.0, 5.0), None); - } - - #[test] - fn render_fills_buffer() { - let (w, h) = (400, 800); + fn render_fills_the_whole_buffer_with_the_field() { + let (w, h) = (8, 4); let mut buf = vec![0u32; (w * h) as usize]; - render( - &mut buf, - w, - h, - &Scene { pin_len: 3, mood: Mood::Entering, pressed: Some(Key::Digit(5)), quiet: false, failed_attempts: 0 }, - ); - assert!(buf.iter().any(|&p| p == BG)); - assert!(buf.iter().any(|&p| p == KEY_FILL)); - assert!(buf.iter().any(|&p| p == KEY_PRESSED)); + render(&mut buf, w, h, &Scene { mood: Mood::Holding }); + assert!(buf.iter().all(|&px| px == BG)); } #[test] - fn quiet_scene_is_bare_field() { - let (w, h) = (400, 800); + fn render_is_opaque_everywhere() { + // A lock surface that is even partly transparent shows the session + // underneath it, which is the one thing it exists to prevent. + let (w, h) = (4, 4); let mut buf = vec![0u32; (w * h) as usize]; - render( - &mut buf, - w, - h, - &Scene { pin_len: 0, mood: Mood::Entering, pressed: None, quiet: true, failed_attempts: 0 }, - ); - assert!(buf.iter().all(|&p| p == BG)); - } - - #[test] - fn failed_attempts_renders_red_digits() { - let (w, h) = (400, 800); - let mut buf_no_fail = vec![0u32; (w * h) as usize]; - render( - &mut buf_no_fail, - w, - h, - &Scene { pin_len: 0, mood: Mood::Entering, pressed: None, quiet: false, failed_attempts: 0 }, - ); - let mut buf_with_fail = vec![0u32; (w * h) as usize]; - render( - &mut buf_with_fail, - w, - h, - &Scene { pin_len: 0, mood: Mood::Failed, pressed: None, quiet: false, failed_attempts: 3 }, - ); - // The failed-attempts render should introduce FAIL-colored pixels - // that aren't in the zero-attempts render. - let has_fail_color = buf_with_fail.iter().zip(buf_no_fail.iter()) - .any(|(&new, &old)| new == FAIL && old != FAIL); - assert!(has_fail_color, "failed attempt counter should render FAIL-colored pixels"); + render(&mut buf, w, h, &Scene { mood: Mood::Holding }); + assert!(buf.iter().all(|&px| px >> 24 == 0xFF)); } } diff --git a/src/sessiond/lock.rs b/src/sessiond/lock.rs index 69ce036..26f9111 100644 --- a/src/sessiond/lock.rs +++ b/src/sessiond/lock.rs @@ -1,18 +1,43 @@ //! ext-session-lock-v1 client: acquire the compositor lock, render the -//! fallback surface, run PIN → PAM, and support the abandoned-lock handoff. +//! holding surface, and support the abandoned-lock handoff. +//! +//! **This daemon locks. It does not unlock.** There is no PIN pad and no PAM +//! conversation here, deliberately, removed 2026-07-29. +//! +//! What was here was a second lockscreen: it authenticated, called +//! `unlock_and_destroy()`, and returned `Unlocked` — and then the shell, whose +//! `GlobalStates.screenLocked` is a separate bool, re-registered, was told +//! `must_lock=true`, and locked again. The protocol has a lock directive and +//! no unlock directive, so the fallback could open the compositor's lock and +//! had no way to tell the session it had. Casey hit it exactly that way: "it +//! logs in, and then qs says Locked still." +//! +//! It also was not a last resort in practice. It is raised whenever +//! `shell_alive` is false, and that flag lied for 90 minutes on 2026-07-29 +//! because a scene reload's re-registration was refused and the shell gave up +//! (fixed in 45fbbea). So the "fallback" was the first thing reached, on a bad +//! signal, to do a job it could not finish. That is the fallback shape the +//! doctrine forbids: a second implementation of a job, on an unreliable +//! trigger, with different and worse behaviour than the first. +//! +//! What remains is the part that was never a fallback: something must hold the +//! session locked before any shell exists, or there is a window where the panel +//! is live and unlocked (`LOCK-DPMS-LESSONS.md` §1). A locked session with no +//! shell now stays locked and says so. Recovery is the shell coming back, or +//! the user rebooting — both louder and more honest than a keypad that cannot +//! hand control back. //! //! One lock session = one Wayland connection. Releasing to the shell is //! `SessionOutcome::Released`: the connection is dropped WITHOUT unlocking, //! which leaves the compositor holding the session locked until the shell's -//! own lock takes over (`misc:allow_session_lock_restore`). The unlocked -//! path only exists through a successful PAM conversation here. +//! own lock takes over (`misc:allow_session_lock_restore`). use std::collections::HashMap; use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd}; use std::sync::mpsc::{Receiver, Sender}; use anyhow::{bail, Context, Result}; -use tracing::{info, warn}; +use tracing::info; use wayland_client::{ delegate_noop, protocol::{ @@ -36,13 +61,10 @@ use wayland_protocols::ext::session_lock::v1::client::{ ext_session_lock_v1::{self, ExtSessionLockV1}, }; -use crate::sessiond::auth; -use crate::sessiond::draw::{self, hit_test, Key, Mood, Scene}; +use crate::sessiond::draw::{self, Mood, Scene}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SessionOutcome { - /// The user authenticated at the fallback surface; session is unlocked. - Unlocked, /// Told to hand off: connection dropped, session still locked. Released, /// The compositor refused or revoked the lock (another locker active). @@ -53,7 +75,6 @@ pub enum SessionOutcome { /// worker's verdict share one channel; the pipe write wakes the poll loop. pub enum Msg { Release, - AuthDone(Result<(), String>), } pub struct LockController { @@ -122,25 +143,10 @@ struct LockState { outputs: Vec<(u32, WlOutput)>, surfaces: Vec, seats: HashMap, - pin: String, mood: Mood, - pressed: Option, dirty: bool, - authing: bool, - submit: Option, pointer_pos: (f64, f64), pointer_surface: Option, // protocol id of the entered wl_surface - /// True until the user touches the surface. While quiet we render only - /// the dark field, so the boot handoff to the shell never flashes the - /// fallback keypad. The first input reveals the keypad (and is - /// swallowed — it's a "show me" gesture, not a digit). - quiet: bool, - /// Number of failed authentication attempts since the lock surface - /// appeared. Displayed on the PIN pad so the user knows how many - /// tries they've had. Reset on successful auth (the session unlocks). - /// The actual lockout policy lives in PAM (pam_faillock); this counter - /// is advisory for user visibility only. - failed_attempts: u32, } impl LockState { @@ -155,69 +161,22 @@ impl LockState { outputs: Vec::new(), surfaces: Vec::new(), seats: HashMap::new(), - pin: String::new(), - mood: Mood::Entering, - pressed: None, + mood: Mood::Holding, dirty: false, - authing: false, - submit: None, pointer_pos: (0.0, 0.0), pointer_surface: None, - quiet: true, - failed_attempts: 0, } } - /// First input while quiet reveals the keypad instead of registering. - /// Returns true when the input was consumed by the reveal. - fn reveal(&mut self) -> bool { - if self.quiet { - self.quiet = false; - self.dirty = true; - return true; - } - false - } - - fn press(&mut self, key: Key) { - if self.authing { - return; - } - if self.mood == Mood::Failed { - self.mood = Mood::Entering; - } - self.pressed = Some(key); - match key { - Key::Digit(d) => { - if self.pin.len() < 32 { - self.pin.push((b'0' + d) as char); - } - } - Key::Backspace => { - self.pin.pop(); - } - Key::Enter => { - if !self.pin.is_empty() { - self.submit = Some(std::mem::take(&mut self.pin)); - } - } - } + /// Input reaches a locked session with no shell and goes nowhere, on + /// purpose. There is nothing to type into: this daemon cannot unlock. + /// Kept as a no-op rather than deleted so the surface still repaints on + /// touch — a screen that ignores you entirely is indistinguishable from a + /// hung one, and the point is to be legible, not silent. + fn press(&mut self) { self.dirty = true; } - fn key_from_evdev(code: u32) -> Option { - match code { - 2..=10 => Some(Key::Digit(code as u8 - 1)), // KEY_1..KEY_9 - 11 => Some(Key::Digit(0)), // KEY_0 - 79..=81 => Some(Key::Digit(code as u8 - 72)), // KP7..KP9 - 75..=77 => Some(Key::Digit(code as u8 - 71)), // KP4..KP6 - 71..=73 => Some(Key::Digit(code as u8 - 70)), // KP1..KP3 - 82 => Some(Key::Digit(0)), // KP0 - 14 => Some(Key::Backspace), - 28 | 96 => Some(Key::Enter), - _ => None, - } - } fn surface_size_by_proto_id(&self, id: u32) -> Option<(i32, i32)> { self.surfaces @@ -227,14 +186,9 @@ impl LockState { } } -/// Run one full lock session on the current thread. Blocks until unlock, -/// release, or denial. -pub fn run( - rx: Receiver, - wake_read: OwnedFd, - controller_for_auth: LockController, - pam_service: String, -) -> Result { +/// Run one full lock session on the current thread. Blocks until release or +/// denial. There is no unlock outcome — this daemon does not unlock. +pub fn run(rx: Receiver, wake_read: OwnedFd) -> Result { let conn = Connection::connect_to_env().context("connecting to Wayland display")?; let mut queue = conn.new_event_queue(); let qh = queue.handle(); @@ -274,8 +228,6 @@ pub fn run( } info!("session lock acquired"); - let user = auth::session_user().map_err(|e| anyhow::anyhow!(e))?; - loop { if state.finished { info!("lock revoked by compositor (another locker?)"); @@ -289,59 +241,16 @@ pub fn run( state.dirty = false; } - if let Some(pin) = state.submit.take() { - if !state.authing { - state.authing = true; - state.mood = Mood::Checking; - state.pressed = None; - state.dirty = true; - let service = pam_service.clone(); - let user = user.clone(); - let ctl = LockController { - tx: controller_for_auth.tx.clone(), - wake: controller_for_auth.wake.try_clone()?, - }; - std::thread::spawn(move || { - let verdict = auth::authenticate(&service, &user, &pin); - ctl.send(Msg::AuthDone(verdict)); - }); - continue; - } - } - - // Drain control/auth messages before sleeping. - let mut acted = false; + // Drain control messages before sleeping. Release is the only one + // left and it returns, so there is nothing to loop back for. while let Ok(msg) = rx.try_recv() { - acted = true; match msg { Msg::Release => { info!("releasing session lock to shell (connection drop, stays locked)"); return Ok(SessionOutcome::Released); } - Msg::AuthDone(Ok(())) => { - info!("PAM success — unlocking session"); - if let Some(lock) = &state.lock { - lock.unlock_and_destroy(); - } - // The protocol requires a roundtrip after unlock so the - // compositor sees it before we disconnect; otherwise the - // exit is indistinguishable from an abandon. - queue.roundtrip(&mut state).context("unlock roundtrip")?; - return Ok(SessionOutcome::Unlocked); - } - Msg::AuthDone(Err(reason)) => { - warn!("PAM refused: {reason}"); - state.authing = false; - state.mood = Mood::Failed; - state.failed_attempts += 1; - state.pin.clear(); - state.dirty = true; - } } } - if acted { - continue; - } queue.flush().context("flush")?; if queue.dispatch_pending(&mut state).context("dispatch")? > 0 { @@ -419,13 +328,7 @@ fn ensure_surfaces(state: &mut LockState, qh: &QueueHandle) { } fn redraw_all(state: &mut LockState, qh: &QueueHandle) -> Result<()> { - let scene = Scene { - pin_len: state.pin.len(), - mood: state.mood, - pressed: state.pressed, - quiet: state.quiet, - failed_attempts: state.failed_attempts, - }; + let scene = Scene { mood: state.mood }; let Some(shm) = state.shm.clone() else { return Ok(()) }; for ctx in &mut state.surfaces { if !ctx.configured || ctx.width <= 0 || ctx.height <= 0 { @@ -612,21 +515,10 @@ impl Dispatch for LockState { ) { match event { wl_touch::Event::Down { surface, x, y, .. } => { - if state.reveal() { - return; - } - let pid = surface.id().protocol_id(); - if let Some((w, h)) = state.surface_size_by_proto_id(pid) { - if let Some(key) = hit_test(w, h, x, y) { - state.press(key); - } - } - } - wl_touch::Event::Up { .. } => { - if state.pressed.take().is_some() { - state.dirty = true; - } + let _ = (surface, x, y); + state.press(); } + wl_touch::Event::Up { .. } => {} _ => {} } } @@ -653,24 +545,8 @@ impl Dispatch for LockState { state.pointer_pos = (surface_x, surface_y); } wl_pointer::Event::Button { state: WEnum::Value(st), .. } => match st { - wl_pointer::ButtonState::Pressed => { - if state.reveal() { - return; - } - if let Some(pid) = state.pointer_surface { - if let Some((w, h)) = state.surface_size_by_proto_id(pid) { - let (px, py) = state.pointer_pos; - if let Some(key) = hit_test(w, h, px, py) { - state.press(key); - } - } - } - } - wl_pointer::ButtonState::Released => { - if state.pressed.take().is_some() { - state.dirty = true; - } - } + wl_pointer::ButtonState::Pressed => state.press(), + wl_pointer::ButtonState::Released => {} _ => {} }, _ => {} @@ -691,17 +567,10 @@ impl Dispatch for LockState { wl_keyboard::Event::Key { key, state: WEnum::Value(st), .. } => { match st { wl_keyboard::KeyState::Pressed => { - if state.reveal() { - return; - } - if let Some(k) = LockState::key_from_evdev(key) { - state.press(k); - } + let _ = key; + state.press(); } wl_keyboard::KeyState::Released => { - if state.pressed.take().is_some() { - state.dirty = true; - } } _ => {} } diff --git a/src/sessiond/mod.rs b/src/sessiond/mod.rs index e9e4c59..70b8f1f 100644 --- a/src/sessiond/mod.rs +++ b/src/sessiond/mod.rs @@ -10,7 +10,6 @@ //! Compiled into the `souveraine-sessiond` bin via `#[path]` includes, the //! same pattern as machined and secrets. -pub mod auth; pub mod device_state; pub mod draw; pub mod idle; diff --git a/src/sessiond/protocol.rs b/src/sessiond/protocol.rs index 7d8fc6a..bbd7822 100644 --- a/src/sessiond/protocol.rs +++ b/src/sessiond/protocol.rs @@ -34,7 +34,6 @@ pub const MAX_REQUEST_BYTES: u64 = 16 * 1024; /// quickshell's default (`login`); the dedicated file lets the PIN stack be /// audited separately and is shipped as root-owned system config by the OS /// overlay, never by this crate (same stance as `souveraine-stepup`). -pub const DEFAULT_PAM_SERVICE: &str = "souveraine-sessiond"; /// How long after releasing the lock we wait for the shell's `locked_ack` /// before deciding the shell is broken and taking the lock back. diff --git a/src/sessiond/server.rs b/src/sessiond/server.rs index 1356f7a..defc8b3 100644 --- a/src/sessiond/server.rs +++ b/src/sessiond/server.rs @@ -60,7 +60,6 @@ struct Daemon { /// daemon that owns the *decision* reaches the process that owns the /// *surface*. shell_directives: Option, - pam_service: String, /// The unified device state machine. The single authority for device /// power state — idle, lock, doze, sleep. All actors route through it. device_state: DeviceStateMachine, @@ -82,7 +81,7 @@ pub fn socket_path() -> Result { Ok(PathBuf::from(runtime).join(crate::sessiond::protocol::SOCKET_RELPATH)) } -pub fn run(pam_service: String, initial_lock: bool, socket_path: &Path) -> Result<()> { +pub fn run(initial_lock: bool, socket_path: &Path) -> Result<()> { if let Some(parent) = socket_path.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("creating {}", parent.display()))?; @@ -111,7 +110,6 @@ pub fn run(pam_service: String, initial_lock: bool, socket_path: &Path) -> Resul heartbeat_gen: 0, shell_alive: false, shell_directives: None, - pam_service, device_state: DeviceStateMachine::new(), }), cond: Condvar::new(), @@ -392,14 +390,6 @@ fn spawn_lock_session(shared: &Arc) -> bool { return false; } }; - // The auth worker feeds verdicts back into the SAME session's channel. - let auth_controller = match controller.try_clone() { - Ok(c) => c, - Err(e) => { - warn!("cannot clone lock controller: {e:#}"); - return false; - } - }; d.controller = Some(controller); d.phase = Phase::Holding; // sessiond itself now holds the lock — boot lock, retake after a shell @@ -409,21 +399,10 @@ fn spawn_lock_session(shared: &Arc) -> bool { let shared = Arc::clone(shared); std::thread::spawn(move || { - let pam_service = shared.lock().pam_service.clone(); - let outcome = lock::run(rx, wake_read, auth_controller, pam_service); + let outcome = lock::run(rx, wake_read); let mut d = shared.lock(); d.controller = None; match outcome { - Ok(SessionOutcome::Unlocked) => { - info!("session unlocked at fallback surface"); - d.phase = if d.shell_alive { - Phase::Released - } else { - Phase::Idle - }; - // PAM succeeded. Only this path may leave the locked tier. - set_tier(&mut d, DeviceState::Active, "unlocked at fallback surface"); - } Ok(SessionOutcome::Released) => { d.phase = Phase::AwaitingShellLock; let gen = d.heartbeat_gen; @@ -974,7 +953,6 @@ mod tests { heartbeat_gen: 0, shell_alive: false, shell_directives: None, - pam_service: "test".to_string(), device_state: DeviceStateMachine::new(), }), cond: Condvar::new(),