sessiond: gate the blank actuator on logind LockedHint
This commit is contained in:
parent
29a3c472f5
commit
2c1ee8bc00
2 changed files with 40 additions and 3 deletions
|
|
@ -55,7 +55,7 @@ use tracing::{info, warn};
|
|||
/// path, so monitoring the alias yields a subscription that is silently never
|
||||
/// delivered. That is the failure the shell already hit
|
||||
/// (`[session-events] no session path returned`).
|
||||
fn resolve_session_path() -> Result<String> {
|
||||
pub fn resolve_session_path() -> Result<String> {
|
||||
let uid = std::fs::metadata("/proc/self")
|
||||
.map(|m| m.uid())
|
||||
.context("reading our own uid")?;
|
||||
|
|
@ -121,7 +121,7 @@ fn resolve_session_path() -> Result<String> {
|
|||
|
||||
/// One-shot read, so the machine starts from the truth rather than from a
|
||||
/// default that happens to be wrong until the first signal arrives.
|
||||
fn read_locked_hint(path: &str) -> Result<bool> {
|
||||
pub fn read_locked_hint(path: &str) -> Result<bool> {
|
||||
let out = Command::new("busctl")
|
||||
.args([
|
||||
"--system",
|
||||
|
|
|
|||
|
|
@ -205,6 +205,21 @@ fn spawn_lock_hint_watcher(shared: &Arc<Shared>) {
|
|||
|
||||
/// Carry out one decision. Failures are recorded in the forensic trail — a
|
||||
/// blank that did not happen is a state divergence, not a log line to lose.
|
||||
/// Ask logind whether this session is locked. Fail closed: if the question
|
||||
/// cannot be answered, the answer is "not locked", because the failure this
|
||||
/// guards is a dark screen on an open session.
|
||||
fn session_is_locked() -> bool {
|
||||
match crate::sessiond::lockhint::resolve_session_path()
|
||||
.and_then(|p| crate::sessiond::lockhint::read_locked_hint(&p))
|
||||
{
|
||||
Ok(locked) => locked,
|
||||
Err(e) => {
|
||||
warn!("could not read LockedHint ({e:#}); treating the session as unlocked");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute(shared: &Arc<Shared>, action: Action) {
|
||||
// The lock is not a shell command, so it does not go through the executor
|
||||
// table below. It is the authority acting as the authority: the shell owns
|
||||
|
|
@ -265,7 +280,29 @@ fn execute(shared: &Arc<Shared>, action: Action) {
|
|||
// dim is what pinned brightness at 10/255.
|
||||
Action::Dim => ("brightnessctl", vec!["set", "10"], "panel-dim"),
|
||||
Action::Restore => unreachable!("handled above; the restore carries a value"),
|
||||
Action::Blank => (DPMS_EXECUTOR, vec!["off"], "panel-off"),
|
||||
Action::Blank => {
|
||||
// The invariant, enforced where it cannot be reasoned around: the
|
||||
// panel does not go dark unless logind says this session is
|
||||
// locked, right now. The machine's own `Locked` is a belief, and a
|
||||
// belief that entered by one route (locked_ack) and can only leave
|
||||
// by another (logind's falling edge) gets stuck — observed
|
||||
// 2026-07-26: machine `locked`, LockedHint `no`, panel off, and
|
||||
// double-tap waking straight to an unlocked screen.
|
||||
//
|
||||
// Checked at the actuator, not at the decision, because every path
|
||||
// to a dark panel ends here and only here.
|
||||
if !session_is_locked() {
|
||||
warn!("refusing to blank: logind says this session is not locked");
|
||||
shared.lock().device_state.record_error(
|
||||
"device-state",
|
||||
"panel-off",
|
||||
"refused: session not locked per logind",
|
||||
);
|
||||
request_session_lock(shared);
|
||||
return;
|
||||
}
|
||||
(DPMS_EXECUTOR, vec!["off"], "panel-off")
|
||||
}
|
||||
Action::Lock => unreachable!("handled above; the lock is not a shell command"),
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue