Watch
1
0
Fork
You've already forked souveraine
0

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:
Fimeg 2026-08-02 15:26:37 -04:00
commit 94988138b4
2 changed files with 106 additions and 4 deletions

View file

@ -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}");

View file

@ -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: {