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();
|
||||
|
||||
let mut pam_service = sessiond::protocol::DEFAULT_PAM_SERVICE.to_string();
|
||||
let mut initial_lock = true;
|
||||
let mut socket: Option<PathBuf> = 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
//! 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<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) {
|
||||
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<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)]
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SurfaceCtx>,
|
||||
seats: HashMap<u32, (WlSeat, SeatDevices)>,
|
||||
pin: String,
|
||||
mood: Mood,
|
||||
pressed: Option<Key>,
|
||||
dirty: bool,
|
||||
authing: bool,
|
||||
submit: Option<String>,
|
||||
pointer_pos: (f64, f64),
|
||||
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 {
|
||||
|
|
@ -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<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)> {
|
||||
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<Msg>,
|
||||
wake_read: OwnedFd,
|
||||
controller_for_auth: LockController,
|
||||
pam_service: String,
|
||||
) -> Result<SessionOutcome> {
|
||||
/// 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<Msg>, wake_read: OwnedFd) -> Result<SessionOutcome> {
|
||||
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<LockState>) {
|
|||
}
|
||||
|
||||
fn redraw_all(state: &mut LockState, qh: &QueueHandle<LockState>) -> 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<WlTouch, ()> 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<WlPointer, ()> 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<WlKeyboard, ()> 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;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ struct Daemon {
|
|||
/// daemon that owns the *decision* reaches the process that owns the
|
||||
/// *surface*.
|
||||
shell_directives: Option<UnixStream>,
|
||||
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<PathBuf> {
|
|||
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<Shared>) -> 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<Shared>) -> 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(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue