lock hint: resolve the seat's session, and replay the report
LockedHint was never set on this device, so sessiond's `locked` (which comes from logind per doctrine §4) was permanently false, request_blank() timed out LOCK_ACK_BUDGET on every blank, and the panel went dark on a session nobody could confirm was locked. LOCK-DPMS-LESSONS §1 held in the code and not on the glass — every blank recorded blank-without-lock. Three faults, measured 2026-08-02: - Session.qml wrote the hint through /session/auto, which is the caller's own session. The shell is not in the session that owns the seat: viewtop in logind 66 (seat0/tty1), qs in 70. Written to one session, read from another. - lockhint.rs resolved via User.Display, which an ssh login is enough to point at a seatless remote session — and which then went stale when that session ended. Both sides now ask seat0 for its active session. - The report was dropped outright. hasLoginctl starts false and only flips when a Process probe returns; secure=true landed 30s earlier, so the one edge that mattered hit the guard and was never retried, because the shell locks once at boot and never changes again. Fourth edge-vs-level bug after locked_ack, ChargeRate and bootBloomActive. Verified on hardware: LockedHint now goes yes ~2s after a power tap.
This commit is contained in:
parent
5edf0c410e
commit
94988138b4
2 changed files with 106 additions and 4 deletions
|
|
@ -60,6 +60,45 @@ pub fn resolve_session_path() -> Result<String> {
|
|||
.map(|m| m.uid())
|
||||
.context("reading our own uid")?;
|
||||
|
||||
// The seat's active session first — the one on the glass.
|
||||
//
|
||||
// `User.Display` is not that, and the difference is not academic. Measured
|
||||
// 2026-08-02: an ssh login made `Display` resolve to a *remote* session
|
||||
// with no seat, while the compositor sat on `seat0`/tty1. The hint was then
|
||||
// written to one session and read from another, `LockedHint` never moved,
|
||||
// and every blank took `request_blank`'s fail-open branch and darkened an
|
||||
// unlocked panel. Display also went stale the moment that ssh session
|
||||
// ended, so the resolver's answer depended on who happened to be logged in.
|
||||
//
|
||||
// A phone has one seat and the lock is about the panel on it, so ask the
|
||||
// seat. `Session.qml` resolves the same way for the write side; the two
|
||||
// must agree or this is a hint nobody reads.
|
||||
//
|
||||
// `(so) "76" "/org/freedesktop/login1/session/_376"` — the object path is
|
||||
// the second quoted field, same shape as `Display` below.
|
||||
if let Ok(out) = Command::new("busctl")
|
||||
.args([
|
||||
"--system",
|
||||
"get-property",
|
||||
"org.freedesktop.login1",
|
||||
"/org/freedesktop/login1/seat/seat0",
|
||||
"org.freedesktop.login1.Seat",
|
||||
"ActiveSession",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
if out.status.success() {
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
if let Some(p) = text
|
||||
.split('"')
|
||||
.nth(3)
|
||||
.filter(|p| p.starts_with("/org/freedesktop/login1/session/"))
|
||||
{
|
||||
return Ok(p.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `(so) "1" "/org/freedesktop/login1/session/_31"` — the object path is the
|
||||
// second quoted field.
|
||||
let user_obj = format!("/org/freedesktop/login1/user/_{uid}");
|
||||
|
|
|
|||
|
|
@ -90,6 +90,30 @@ Singleton {
|
|||
if (root.hasLoginctl)
|
||||
lockedHintProc.report(root.locked);
|
||||
}
|
||||
|
||||
// Replay the hint once the capability probe lands.
|
||||
//
|
||||
// `hasLoginctl` starts false and only becomes true when `capabilityProbe`
|
||||
// returns, which is a `Process` round trip. The lock goes secure long
|
||||
// before that: measured 2026-08-02, `secure=true` at 15:21:07 and
|
||||
// `loginctl=true` at 15:21:37 — thirty seconds later. The one edge that
|
||||
// mattered was therefore dropped by the guard above and never retried,
|
||||
// because the shell locks once at boot and `locked` never changes again.
|
||||
//
|
||||
// The cost was the whole lock-before-blank invariant. `LockedHint` stayed
|
||||
// `no`, so sessiond's `locked` (which comes from logind, doctrine §4) was
|
||||
// permanently false, `request_blank()` timed out its `LOCK_ACK_BUDGET`
|
||||
// every time, and the panel blanked on a session nobody could confirm was
|
||||
// locked — `blank-without-lock` on every single blank.
|
||||
//
|
||||
// This is the fourth edge-vs-level bug in this system after `locked_ack`,
|
||||
// `ChargeRate` and `bootBloomActive`. A guard that drops a report must
|
||||
// replay it when the guard opens, or the report is only ever delivered by
|
||||
// luck of ordering.
|
||||
onHasLoginctlChanged: {
|
||||
if (root.hasLoginctl)
|
||||
lockedHintProc.report(root.locked);
|
||||
}
|
||||
Process {
|
||||
id: lockedHintProc
|
||||
property bool pending: false
|
||||
|
|
@ -101,10 +125,49 @@ Singleton {
|
|||
pendingValue = value;
|
||||
return;
|
||||
}
|
||||
command = ["busctl", "call", "org.freedesktop.login1",
|
||||
"/org/freedesktop/login1/session/auto",
|
||||
"org.freedesktop.login1.Session",
|
||||
"SetLockedHint", "b", value ? "true" : "false"];
|
||||
// Resolve the session the way `lockhint.rs` does — the user's
|
||||
// Display session — and never through the `/session/auto` alias.
|
||||
//
|
||||
// `auto` means *the caller's own* session, and the shell is not in
|
||||
// the session that owns the seat. Measured 2026-08-02: viewtop in
|
||||
// logind session 66 (seat0, tty1, the one `User.Display` names and
|
||||
// the one sessiond watches), `qs -c souveraine` in session 70. So
|
||||
// this call was setting the hint on a session nobody reads, while
|
||||
// the graphical session's `LockedHint` stayed `no` forever.
|
||||
//
|
||||
// The consequence was not cosmetic. sessiond takes `locked` from
|
||||
// `LockedHint` (doctrine §4), so `locked` was permanently false;
|
||||
// `request_blank()` therefore timed out its `LOCK_ACK_BUDGET` on
|
||||
// every single blank and took the fail-open branch, darkening the
|
||||
// panel on a session it could not confirm was locked and writing
|
||||
// `blank-without-lock` each time. LOCK-DPMS-LESSONS §1's ordering
|
||||
// held in the code and not on the device.
|
||||
//
|
||||
// One shell round trip rather than two Processes: the path has to
|
||||
// be resolved at report time, because the session id changes across
|
||||
// a greetd restart and a cached one would be stale exactly when it
|
||||
// matters. `$(…)` is fine here — the value is a busctl-printed
|
||||
// object path, not user input.
|
||||
// The seat's active session — the one on the glass — with
|
||||
// `User.Display` only as a fallback. `lockhint.rs` resolves the
|
||||
// same way on the read side, and the two must agree or this writes
|
||||
// a hint nobody reads. An ssh login is enough to make `Display`
|
||||
// name a seatless remote session, which is exactly how this was
|
||||
// found.
|
||||
command = ["sh", "-c",
|
||||
"p=$(busctl get-property org.freedesktop.login1 " +
|
||||
"/org/freedesktop/login1/seat/seat0 " +
|
||||
"org.freedesktop.login1.Seat ActiveSession " +
|
||||
"| grep -o '/org/freedesktop/login1/session/[^\"]*'); " +
|
||||
"[ -n \"$p\" ] || p=$(busctl get-property " +
|
||||
"org.freedesktop.login1 " +
|
||||
"/org/freedesktop/login1/user/_$(id -u) " +
|
||||
"org.freedesktop.login1.User Display " +
|
||||
"| grep -o '/org/freedesktop/login1/session/[^\"]*'); " +
|
||||
"[ -n \"$p\" ] || exit 1; " +
|
||||
"exec busctl call org.freedesktop.login1 \"$p\" " +
|
||||
"org.freedesktop.login1.Session SetLockedHint b " +
|
||||
(value ? "true" : "false")];
|
||||
running = true;
|
||||
}
|
||||
onExited: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue