sessiond: delete the fallback login; this daemon locks and never unlocks
It authenticated through PAM, 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 was also not a last resort in practice. It is raised whenever shell_alive is
false, and that flag lied for 90 minutes on 07-29 because a scene reload's
re-registration was refused and the shell gave up (45fbbea). So it was the
first thing reached, on a bad signal, to do a job it could not finish — the
fallback shape the doctrine forbids.
Gone: auth.rs, the PIN pad, the keypad geometry and digit font, the
failed-attempt counter, SessionOutcome::Unlocked, Msg::AuthDone, and the
--pam-service plumbing down to the CLI.
Kept: acquiring ext-session-lock before any shell exists, which was never a
fallback but the ordering invariant from LOCK-DPMS-LESSONS.md §1, and the
Released handoff. A locked session with no shell now stays locked and says so
in the journal. Recovery is the shell returning, or a reboot.
79 tests pass.
This commit is contained in:
parent
0a635ff984
commit
c8507e924b
7 changed files with 90 additions and 694 deletions
|
|
@ -27,18 +27,12 @@ fn main() -> anyhow::Result<()> {
|
||||||
)
|
)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let mut pam_service = sessiond::protocol::DEFAULT_PAM_SERVICE.to_string();
|
|
||||||
let mut initial_lock = true;
|
let mut initial_lock = true;
|
||||||
let mut socket: Option<PathBuf> = None;
|
let mut socket: Option<PathBuf> = None;
|
||||||
|
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = std::env::args().skip(1);
|
||||||
while let Some(arg) = args.next() {
|
while let Some(arg) = args.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"--pam-service" => {
|
|
||||||
pam_service = args
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("--pam-service requires a name"))?;
|
|
||||||
}
|
|
||||||
"--socket" => {
|
"--socket" => {
|
||||||
socket = Some(PathBuf::from(args.next().ok_or_else(|| {
|
socket = Some(PathBuf::from(args.next().ok_or_else(|| {
|
||||||
anyhow::anyhow!("--socket requires a path")
|
anyhow::anyhow!("--socket requires a path")
|
||||||
|
|
@ -47,8 +41,7 @@ fn main() -> anyhow::Result<()> {
|
||||||
"--no-initial-lock" => initial_lock = false,
|
"--no-initial-lock" => initial_lock = false,
|
||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
println!(
|
println!(
|
||||||
"souveraine-sessiond [--pam-service {}] [--socket $XDG_RUNTIME_DIR/{}] [--no-initial-lock]",
|
"souveraine-sessiond [--socket $XDG_RUNTIME_DIR/{}] [--no-initial-lock]",
|
||||||
sessiond::protocol::DEFAULT_PAM_SERVICE,
|
|
||||||
sessiond::protocol::SOCKET_RELPATH,
|
sessiond::protocol::SOCKET_RELPATH,
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -61,5 +54,5 @@ fn main() -> anyhow::Result<()> {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => sessiond::server::socket_path()?,
|
None => sessiond::server::socket_path()?,
|
||||||
};
|
};
|
||||||
sessiond::server::run(pam_service, initial_lock, &socket)
|
sessiond::server::run(initial_lock, &socket)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::<PamResponse>()) 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<String, String> {
|
|
||||||
// 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
//! A dark field. That is the whole surface, deliberately.
|
||||||
//! 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
|
//! Until 2026-07-29 this drew a PIN pad — dots, a 3x4 keypad, a 5x7 digit
|
||||||
//! a corpse — it competes on availability, not looks. The rich lockscreen
|
//! font, a failed-attempt counter — because sessiond could authenticate and
|
||||||
//! (swipe, widgets) stays in the shell.
|
//! 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).
|
//! 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 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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Key {
|
pub enum Mood {
|
||||||
Digit(u8),
|
/// The session is locked and this daemon is holding it.
|
||||||
Backspace,
|
Holding,
|
||||||
Enter,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[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 struct Scene {
|
||||||
pub pin_len: usize,
|
|
||||||
pub mood: Mood,
|
pub mood: Mood,
|
||||||
pub pressed: Option<Key>,
|
|
||||||
/// 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<Key> {
|
|
||||||
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) {
|
pub fn render(buf: &mut [u32], w: i32, h: i32, scene: &Scene) {
|
||||||
debug_assert_eq!(buf.len(), (w * h) as usize);
|
debug_assert_eq!(buf.len(), (w * h) as usize);
|
||||||
|
let _ = scene;
|
||||||
|
let _ = (w, h);
|
||||||
buf.fill(BG);
|
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<u8> = 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)]
|
#[cfg(test)]
|
||||||
|
|
@ -262,65 +48,20 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn layout_and_hit_test_agree() {
|
fn render_fills_the_whole_buffer_with_the_field() {
|
||||||
let (w, h) = (1080, 2160);
|
let (w, h) = (8, 4);
|
||||||
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);
|
|
||||||
let mut buf = vec![0u32; (w * h) as usize];
|
let mut buf = vec![0u32; (w * h) as usize];
|
||||||
render(
|
render(&mut buf, w, h, &Scene { mood: Mood::Holding });
|
||||||
&mut buf,
|
assert!(buf.iter().all(|&px| px == BG));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quiet_scene_is_bare_field() {
|
fn render_is_opaque_everywhere() {
|
||||||
let (w, h) = (400, 800);
|
// 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];
|
let mut buf = vec![0u32; (w * h) as usize];
|
||||||
render(
|
render(&mut buf, w, h, &Scene { mood: Mood::Holding });
|
||||||
&mut buf,
|
assert!(buf.iter().all(|&px| px >> 24 == 0xFF));
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,43 @@
|
||||||
//! ext-session-lock-v1 client: acquire the compositor lock, render the
|
//! 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
|
//! One lock session = one Wayland connection. Releasing to the shell is
|
||||||
//! `SessionOutcome::Released`: the connection is dropped WITHOUT unlocking,
|
//! `SessionOutcome::Released`: the connection is dropped WITHOUT unlocking,
|
||||||
//! which leaves the compositor holding the session locked until the shell's
|
//! which leaves the compositor holding the session locked until the shell's
|
||||||
//! own lock takes over (`misc:allow_session_lock_restore`). The unlocked
|
//! own lock takes over (`misc:allow_session_lock_restore`).
|
||||||
//! path only exists through a successful PAM conversation here.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
|
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
|
||||||
use std::sync::mpsc::{Receiver, Sender};
|
use std::sync::mpsc::{Receiver, Sender};
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use tracing::{info, warn};
|
use tracing::info;
|
||||||
use wayland_client::{
|
use wayland_client::{
|
||||||
delegate_noop,
|
delegate_noop,
|
||||||
protocol::{
|
protocol::{
|
||||||
|
|
@ -36,13 +61,10 @@ use wayland_protocols::ext::session_lock::v1::client::{
|
||||||
ext_session_lock_v1::{self, ExtSessionLockV1},
|
ext_session_lock_v1::{self, ExtSessionLockV1},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::sessiond::auth;
|
use crate::sessiond::draw::{self, Mood, Scene};
|
||||||
use crate::sessiond::draw::{self, hit_test, Key, Mood, Scene};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SessionOutcome {
|
pub enum SessionOutcome {
|
||||||
/// The user authenticated at the fallback surface; session is unlocked.
|
|
||||||
Unlocked,
|
|
||||||
/// Told to hand off: connection dropped, session still locked.
|
/// Told to hand off: connection dropped, session still locked.
|
||||||
Released,
|
Released,
|
||||||
/// The compositor refused or revoked the lock (another locker active).
|
/// 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.
|
/// worker's verdict share one channel; the pipe write wakes the poll loop.
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
Release,
|
Release,
|
||||||
AuthDone(Result<(), String>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LockController {
|
pub struct LockController {
|
||||||
|
|
@ -122,25 +143,10 @@ struct LockState {
|
||||||
outputs: Vec<(u32, WlOutput)>,
|
outputs: Vec<(u32, WlOutput)>,
|
||||||
surfaces: Vec<SurfaceCtx>,
|
surfaces: Vec<SurfaceCtx>,
|
||||||
seats: HashMap<u32, (WlSeat, SeatDevices)>,
|
seats: HashMap<u32, (WlSeat, SeatDevices)>,
|
||||||
pin: String,
|
|
||||||
mood: Mood,
|
mood: Mood,
|
||||||
pressed: Option<Key>,
|
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
authing: bool,
|
|
||||||
submit: Option<String>,
|
|
||||||
pointer_pos: (f64, f64),
|
pointer_pos: (f64, f64),
|
||||||
pointer_surface: Option<u32>, // protocol id of the entered wl_surface
|
pointer_surface: Option<u32>, // 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 {
|
impl LockState {
|
||||||
|
|
@ -155,69 +161,22 @@ impl LockState {
|
||||||
outputs: Vec::new(),
|
outputs: Vec::new(),
|
||||||
surfaces: Vec::new(),
|
surfaces: Vec::new(),
|
||||||
seats: HashMap::new(),
|
seats: HashMap::new(),
|
||||||
pin: String::new(),
|
mood: Mood::Holding,
|
||||||
mood: Mood::Entering,
|
|
||||||
pressed: None,
|
|
||||||
dirty: false,
|
dirty: false,
|
||||||
authing: false,
|
|
||||||
submit: None,
|
|
||||||
pointer_pos: (0.0, 0.0),
|
pointer_pos: (0.0, 0.0),
|
||||||
pointer_surface: None,
|
pointer_surface: None,
|
||||||
quiet: true,
|
|
||||||
failed_attempts: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// First input while quiet reveals the keypad instead of registering.
|
/// Input reaches a locked session with no shell and goes nowhere, on
|
||||||
/// Returns true when the input was consumed by the reveal.
|
/// purpose. There is nothing to type into: this daemon cannot unlock.
|
||||||
fn reveal(&mut self) -> bool {
|
/// Kept as a no-op rather than deleted so the surface still repaints on
|
||||||
if self.quiet {
|
/// touch — a screen that ignores you entirely is indistinguishable from a
|
||||||
self.quiet = false;
|
/// hung one, and the point is to be legible, not silent.
|
||||||
self.dirty = true;
|
fn press(&mut self) {
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_from_evdev(code: u32) -> Option<Key> {
|
|
||||||
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)> {
|
fn surface_size_by_proto_id(&self, id: u32) -> Option<(i32, i32)> {
|
||||||
self.surfaces
|
self.surfaces
|
||||||
|
|
@ -227,14 +186,9 @@ impl LockState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run one full lock session on the current thread. Blocks until unlock,
|
/// Run one full lock session on the current thread. Blocks until release or
|
||||||
/// release, or denial.
|
/// denial. There is no unlock outcome — this daemon does not unlock.
|
||||||
pub fn run(
|
pub fn run(rx: Receiver<Msg>, wake_read: OwnedFd) -> Result<SessionOutcome> {
|
||||||
rx: Receiver<Msg>,
|
|
||||||
wake_read: OwnedFd,
|
|
||||||
controller_for_auth: LockController,
|
|
||||||
pam_service: String,
|
|
||||||
) -> Result<SessionOutcome> {
|
|
||||||
let conn = Connection::connect_to_env().context("connecting to Wayland display")?;
|
let conn = Connection::connect_to_env().context("connecting to Wayland display")?;
|
||||||
let mut queue = conn.new_event_queue();
|
let mut queue = conn.new_event_queue();
|
||||||
let qh = queue.handle();
|
let qh = queue.handle();
|
||||||
|
|
@ -274,8 +228,6 @@ pub fn run(
|
||||||
}
|
}
|
||||||
info!("session lock acquired");
|
info!("session lock acquired");
|
||||||
|
|
||||||
let user = auth::session_user().map_err(|e| anyhow::anyhow!(e))?;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if state.finished {
|
if state.finished {
|
||||||
info!("lock revoked by compositor (another locker?)");
|
info!("lock revoked by compositor (another locker?)");
|
||||||
|
|
@ -289,58 +241,15 @@ pub fn run(
|
||||||
state.dirty = false;
|
state.dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pin) = state.submit.take() {
|
// Drain control messages before sleeping. Release is the only one
|
||||||
if !state.authing {
|
// left and it returns, so there is nothing to loop back for.
|
||||||
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;
|
|
||||||
while let Ok(msg) = rx.try_recv() {
|
while let Ok(msg) = rx.try_recv() {
|
||||||
acted = true;
|
|
||||||
match msg {
|
match msg {
|
||||||
Msg::Release => {
|
Msg::Release => {
|
||||||
info!("releasing session lock to shell (connection drop, stays locked)");
|
info!("releasing session lock to shell (connection drop, stays locked)");
|
||||||
return Ok(SessionOutcome::Released);
|
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")?;
|
queue.flush().context("flush")?;
|
||||||
|
|
@ -419,13 +328,7 @@ fn ensure_surfaces(state: &mut LockState, qh: &QueueHandle<LockState>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redraw_all(state: &mut LockState, qh: &QueueHandle<LockState>) -> Result<()> {
|
fn redraw_all(state: &mut LockState, qh: &QueueHandle<LockState>) -> Result<()> {
|
||||||
let scene = Scene {
|
let scene = Scene { mood: state.mood };
|
||||||
pin_len: state.pin.len(),
|
|
||||||
mood: state.mood,
|
|
||||||
pressed: state.pressed,
|
|
||||||
quiet: state.quiet,
|
|
||||||
failed_attempts: state.failed_attempts,
|
|
||||||
};
|
|
||||||
let Some(shm) = state.shm.clone() else { return Ok(()) };
|
let Some(shm) = state.shm.clone() else { return Ok(()) };
|
||||||
for ctx in &mut state.surfaces {
|
for ctx in &mut state.surfaces {
|
||||||
if !ctx.configured || ctx.width <= 0 || ctx.height <= 0 {
|
if !ctx.configured || ctx.width <= 0 || ctx.height <= 0 {
|
||||||
|
|
@ -612,21 +515,10 @@ impl Dispatch<WlTouch, ()> for LockState {
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
wl_touch::Event::Down { surface, x, y, .. } => {
|
wl_touch::Event::Down { surface, x, y, .. } => {
|
||||||
if state.reveal() {
|
let _ = (surface, x, y);
|
||||||
return;
|
state.press();
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
wl_touch::Event::Up { .. } => {}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -653,24 +545,8 @@ impl Dispatch<WlPointer, ()> for LockState {
|
||||||
state.pointer_pos = (surface_x, surface_y);
|
state.pointer_pos = (surface_x, surface_y);
|
||||||
}
|
}
|
||||||
wl_pointer::Event::Button { state: WEnum::Value(st), .. } => match st {
|
wl_pointer::Event::Button { state: WEnum::Value(st), .. } => match st {
|
||||||
wl_pointer::ButtonState::Pressed => {
|
wl_pointer::ButtonState::Pressed => state.press(),
|
||||||
if state.reveal() {
|
wl_pointer::ButtonState::Released => {}
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -691,17 +567,10 @@ impl Dispatch<WlKeyboard, ()> for LockState {
|
||||||
wl_keyboard::Event::Key { key, state: WEnum::Value(st), .. } => {
|
wl_keyboard::Event::Key { key, state: WEnum::Value(st), .. } => {
|
||||||
match st {
|
match st {
|
||||||
wl_keyboard::KeyState::Pressed => {
|
wl_keyboard::KeyState::Pressed => {
|
||||||
if state.reveal() {
|
let _ = key;
|
||||||
return;
|
state.press();
|
||||||
}
|
|
||||||
if let Some(k) = LockState::key_from_evdev(key) {
|
|
||||||
state.press(k);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
wl_keyboard::KeyState::Released => {
|
wl_keyboard::KeyState::Released => {
|
||||||
if state.pressed.take().is_some() {
|
|
||||||
state.dirty = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
//! Compiled into the `souveraine-sessiond` bin via `#[path]` includes, the
|
//! Compiled into the `souveraine-sessiond` bin via `#[path]` includes, the
|
||||||
//! same pattern as machined and secrets.
|
//! same pattern as machined and secrets.
|
||||||
|
|
||||||
pub mod auth;
|
|
||||||
pub mod device_state;
|
pub mod device_state;
|
||||||
pub mod draw;
|
pub mod draw;
|
||||||
pub mod idle;
|
pub mod idle;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ pub const MAX_REQUEST_BYTES: u64 = 16 * 1024;
|
||||||
/// quickshell's default (`login`); the dedicated file lets the PIN stack be
|
/// 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
|
/// audited separately and is shipped as root-owned system config by the OS
|
||||||
/// overlay, never by this crate (same stance as `souveraine-stepup`).
|
/// 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`
|
/// 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.
|
/// before deciding the shell is broken and taking the lock back.
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,6 @@ struct Daemon {
|
||||||
/// daemon that owns the *decision* reaches the process that owns the
|
/// daemon that owns the *decision* reaches the process that owns the
|
||||||
/// *surface*.
|
/// *surface*.
|
||||||
shell_directives: Option<UnixStream>,
|
shell_directives: Option<UnixStream>,
|
||||||
pam_service: String,
|
|
||||||
/// The unified device state machine. The single authority for device
|
/// The unified device state machine. The single authority for device
|
||||||
/// power state — idle, lock, doze, sleep. All actors route through it.
|
/// power state — idle, lock, doze, sleep. All actors route through it.
|
||||||
device_state: DeviceStateMachine,
|
device_state: DeviceStateMachine,
|
||||||
|
|
@ -82,7 +81,7 @@ pub fn socket_path() -> Result<PathBuf> {
|
||||||
Ok(PathBuf::from(runtime).join(crate::sessiond::protocol::SOCKET_RELPATH))
|
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() {
|
if let Some(parent) = socket_path.parent() {
|
||||||
std::fs::create_dir_all(parent)
|
std::fs::create_dir_all(parent)
|
||||||
.with_context(|| format!("creating {}", parent.display()))?;
|
.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,
|
heartbeat_gen: 0,
|
||||||
shell_alive: false,
|
shell_alive: false,
|
||||||
shell_directives: None,
|
shell_directives: None,
|
||||||
pam_service,
|
|
||||||
device_state: DeviceStateMachine::new(),
|
device_state: DeviceStateMachine::new(),
|
||||||
}),
|
}),
|
||||||
cond: Condvar::new(),
|
cond: Condvar::new(),
|
||||||
|
|
@ -392,14 +390,6 @@ fn spawn_lock_session(shared: &Arc<Shared>) -> bool {
|
||||||
return false;
|
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.controller = Some(controller);
|
||||||
d.phase = Phase::Holding;
|
d.phase = Phase::Holding;
|
||||||
// sessiond itself now holds the lock — boot lock, retake after a shell
|
// sessiond itself now holds the lock — boot lock, retake after a shell
|
||||||
|
|
@ -409,21 +399,10 @@ fn spawn_lock_session(shared: &Arc<Shared>) -> bool {
|
||||||
|
|
||||||
let shared = Arc::clone(shared);
|
let shared = Arc::clone(shared);
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let pam_service = shared.lock().pam_service.clone();
|
let outcome = lock::run(rx, wake_read);
|
||||||
let outcome = lock::run(rx, wake_read, auth_controller, pam_service);
|
|
||||||
let mut d = shared.lock();
|
let mut d = shared.lock();
|
||||||
d.controller = None;
|
d.controller = None;
|
||||||
match outcome {
|
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) => {
|
Ok(SessionOutcome::Released) => {
|
||||||
d.phase = Phase::AwaitingShellLock;
|
d.phase = Phase::AwaitingShellLock;
|
||||||
let gen = d.heartbeat_gen;
|
let gen = d.heartbeat_gen;
|
||||||
|
|
@ -974,7 +953,6 @@ mod tests {
|
||||||
heartbeat_gen: 0,
|
heartbeat_gen: 0,
|
||||||
shell_alive: false,
|
shell_alive: false,
|
||||||
shell_directives: None,
|
shell_directives: None,
|
||||||
pam_service: "test".to_string(),
|
|
||||||
device_state: DeviceStateMachine::new(),
|
device_state: DeviceStateMachine::new(),
|
||||||
}),
|
}),
|
||||||
cond: Condvar::new(),
|
cond: Condvar::new(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue