shell: fail closed when the session authority cannot be reached
Every failure path in shellReady answered cb(false) — no lock owed — so a socket that was not up, a duplicate handshake, or a 3s timeout all concluded the session was open. The timeout logged 'proceeding without sessiond' and left the phone unlocked at exactly the moment the authority was unreachable, while the daemon side of the same contract retakes the lock on heartbeat EOF 'whether or not the session was locked at the time. Fail closed.' Not knowing the session is locked is not knowing it is not. All of those now assume locked. Refusals still answer false only for 'already registered', which means another shell holds the lease and this process is not the authority. A timed-out handshake is also retried instead of abandoned: a shell that merely started while sessiond was restarting stayed unregistered for its whole life, so sessiond saw no heartbeat and raised its fallback surface over ours. SessionEvents likewise retries session-path resolution rather than disabling lock-signal monitoring for the life of the process, and logs the gap as an error while it is open.
This commit is contained in:
parent
deda4a7b6b
commit
68b6025226
2 changed files with 102 additions and 28 deletions
|
|
@ -110,21 +110,39 @@ Singleton {
|
|||
console.log("[session-events] session path:", root.sessionPath);
|
||||
sessionLockMonitor.running = true;
|
||||
} else if (raw.length > 0) {
|
||||
console.log("[session-events] could not parse Display session from:", raw);
|
||||
console.error("[session-events] could not parse Display session from:", raw,
|
||||
"— retrying; external lock signals are NOT being monitored");
|
||||
} else {
|
||||
console.log("[session-events] no Display session for this user; "
|
||||
+ "external lock signals will not be monitored");
|
||||
// Do not settle for this. logind may simply not have the
|
||||
// graphical session yet at the moment we asked, and giving
|
||||
// up leaves the shell permanently blind to external lock and
|
||||
// unlock — the exact silent gap that made a fallback-surface
|
||||
// unlock leave a stale lock surface on screen.
|
||||
console.error("[session-events] no Display session for this user yet; "
|
||||
+ "retrying — external lock signals are NOT being monitored");
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.log("[session-events] session path probe failed (exit " + exitCode
|
||||
+ "); external lock monitoring disabled");
|
||||
console.error("[session-events] session path probe failed (exit " + exitCode
|
||||
+ "); retrying — external lock monitoring is NOT active");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep asking until the graphical session exists. Idle the moment it does:
|
||||
// `running` is false once sessionPath is set, so this costs nothing in the
|
||||
// normal case and closes the window where a shell that started before
|
||||
// logind published the session would never monitor lock signals at all.
|
||||
Timer {
|
||||
id: sessionResolveRetry
|
||||
interval: 5000
|
||||
repeat: true
|
||||
running: root.sessionPath.length === 0
|
||||
onTriggered: sessionResolver.running = true
|
||||
}
|
||||
|
||||
// --- Sleep delay inhibitor ---------------------------------------------
|
||||
// systemd-inhibit with --mode=delay holds a delay inhibitor on the sleep
|
||||
// verb. logind allows a configurable delay (typically 5s) before forcing
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ Singleton {
|
|||
// disconnect branch clears `registered` before the reconnect branch runs,
|
||||
// so reconnect cannot read it directly to decide whether to re-register.
|
||||
property bool wasRegistered: false
|
||||
// We owe the authority a registration that has not landed yet — set when a
|
||||
// handshake times out, cleared once one is answered. Drives registerRetry.
|
||||
property bool needsRegistration: false
|
||||
// sessiond held the session lock when we registered; we owe it a lock
|
||||
// and a locked_ack.
|
||||
property bool oweLock: false
|
||||
|
|
@ -50,19 +53,37 @@ Singleton {
|
|||
// Announce the shell. cb(mustLock) fires exactly once: mustLock true
|
||||
// means sessiond was holding and the session IS locked — the shell must
|
||||
// raise its own lock surface immediately.
|
||||
//
|
||||
// EVERY failure path here answers TRUE. Not knowing whether the session is
|
||||
// locked is not the same as knowing it is not, and the two must never be
|
||||
// collapsed: sessiond takes ext-session-lock before any shell surface can
|
||||
// exist (protocol.rs §1), so a shell that cannot get an answer is a shell
|
||||
// that arrived after something already locked the session. The daemon side
|
||||
// of this contract is explicit — heartbeat EOF retakes the lock "whether or
|
||||
// not the session was locked at the time. Fail closed." — and this side has
|
||||
// to match it or the pair fails open at exactly the moment the authority is
|
||||
// unreachable.
|
||||
//
|
||||
// Answering true costs an unnecessary lock surface the user dismisses with
|
||||
// PAM. Answering false costs an unlocked phone.
|
||||
function shellReady(cb) {
|
||||
if (!root.authorityScope) {
|
||||
// Not a fail-open: a utility QML process is not the session
|
||||
// authority and has no lock to owe. Only shell.qml claims scope.
|
||||
console.log("[sessiond-bridge] shell_ready refused outside authority scope")
|
||||
cb(false)
|
||||
return
|
||||
}
|
||||
if (!sock.connected) {
|
||||
cb(false);
|
||||
console.warn("[sessiond-bridge] shell_ready with no socket — assuming locked");
|
||||
cb(true);
|
||||
return;
|
||||
}
|
||||
if (root.pendingReady) {
|
||||
// Only one handshake in flight; late duplicate answers false.
|
||||
cb(false);
|
||||
// A handshake is already in flight; it will answer authoritatively.
|
||||
// This duplicate assumes locked rather than racing it to "unlocked".
|
||||
console.warn("[sessiond-bridge] duplicate shell_ready — assuming locked");
|
||||
cb(true);
|
||||
return;
|
||||
}
|
||||
root.pendingReady = cb;
|
||||
|
|
@ -85,14 +106,53 @@ Singleton {
|
|||
repeat: false
|
||||
onTriggered: {
|
||||
if (root.pendingReady) {
|
||||
console.log("[sessiond-bridge] shell_ready timed out — proceeding without sessiond");
|
||||
// Was "proceeding without sessiond" with cb(false): a silent
|
||||
// fail-open that left the session unlocked precisely when the
|
||||
// authority was not answering. Assume locked, and keep trying —
|
||||
// an unanswered handshake is a transient (sessiond restarting),
|
||||
// not a verdict.
|
||||
console.warn("[sessiond-bridge] shell_ready timed out — assuming locked, will retry");
|
||||
const cb = root.pendingReady;
|
||||
root.pendingReady = null;
|
||||
cb(false);
|
||||
root.needsRegistration = true;
|
||||
cb(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register, and apply whatever the authority says we owe. One path, used by
|
||||
// the reconnect handler and the retry timer alike, so the two can never
|
||||
// drift into handling the answer differently.
|
||||
function registerWithAuthority() {
|
||||
root.shellReady(function(mustLock) {
|
||||
if (!mustLock)
|
||||
return;
|
||||
// sessiond locked while we were disconnected (it treats our EOF as
|
||||
// shell death), or we could not confirm and are failing closed.
|
||||
GlobalStates.screenLocked = true;
|
||||
// Our surface may ALREADY be secure from before the daemon
|
||||
// restarted. onScreenLockSecureChanged is an edge, and that edge is
|
||||
// in the past, so nothing would ever send the ack this new handoff
|
||||
// owes — sessiond waits out its timer and retakes the lock
|
||||
// ("shell never confirmed its lock after handoff").
|
||||
if (GlobalStates.screenLockSecure)
|
||||
root.sendLockedAck();
|
||||
});
|
||||
}
|
||||
|
||||
// A handshake that timed out is retried until it lands. Without this a
|
||||
// shell that merely started while sessiond was restarting stays
|
||||
// unregistered for its whole life: sessiond sees no heartbeat, believes the
|
||||
// shell is dead, and raises its own fallback surface over ours forever.
|
||||
Timer {
|
||||
id: registerRetry
|
||||
interval: 5000
|
||||
repeat: true
|
||||
running: root.authorityScope && root.needsRegistration
|
||||
&& !root.registered && sock.connected
|
||||
onTriggered: root.registerWithAuthority()
|
||||
}
|
||||
|
||||
// Reconnect: sessiond may restart (upgrade) or start late. While
|
||||
// connected this timer is idle; the Socket does not retry by itself.
|
||||
Timer {
|
||||
|
|
@ -120,22 +180,8 @@ Singleton {
|
|||
root.registered = false;
|
||||
root.wasRegistered = false;
|
||||
root.ackSent = false;
|
||||
if (wasRegistered) {
|
||||
root.shellReady(function(mustLock) {
|
||||
if (mustLock) {
|
||||
// sessiond locked while we were disconnected (it
|
||||
// treats our EOF as shell death). Mirror it.
|
||||
GlobalStates.screenLocked = true;
|
||||
// Our surface may ALREADY be secure from before the
|
||||
// daemon restarted. onScreenLockSecureChanged is an
|
||||
// edge, and that edge is in the past, so nothing
|
||||
// would ever send the ack this new handoff owes —
|
||||
// sessiond waits out its timer and retakes the lock
|
||||
// ("shell never confirmed its lock after handoff").
|
||||
if (GlobalStates.screenLockSecure)
|
||||
root.sendLockedAck();
|
||||
}
|
||||
});
|
||||
if (wasRegistered || root.needsRegistration) {
|
||||
root.registerWithAuthority();
|
||||
}
|
||||
} else {
|
||||
console.log("[sessiond-bridge] disconnected");
|
||||
|
|
@ -171,12 +217,22 @@ Singleton {
|
|||
readyTimeout.stop();
|
||||
if (reply.ok) {
|
||||
root.registered = true;
|
||||
root.needsRegistration = false;
|
||||
root.oweLock = reply.must_lock === true;
|
||||
console.log("[sessiond-bridge] registered, must_lock=" + root.oweLock);
|
||||
cb(root.oweLock);
|
||||
} else {
|
||||
console.log("[sessiond-bridge] shell_ready refused: " + reply.reason);
|
||||
cb(false);
|
||||
// One refusal is benign and means we are not the
|
||||
// authority at all: another live shell already holds
|
||||
// the lease, so we owe it no lock. Anything else is a
|
||||
// refusal we do not understand, and an authority that
|
||||
// refused us for unknown reasons is not grounds to
|
||||
// assume the session is open.
|
||||
const notAuthority =
|
||||
String(reply.reason || "").indexOf("already registered") !== -1;
|
||||
console.warn("[sessiond-bridge] shell_ready refused: " + reply.reason
|
||||
+ (notAuthority ? "" : " — assuming locked"));
|
||||
cb(!notAuthority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue