git.lucas.co / cce-display-manager
login greeter
git clone https://git.lucas.co/cce-display-manager.git

commit563fdbef627a75af2c854c3e6e413944796d2708
parentcb27293105
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-25 15:01
fix: end a finished session with kill-session, not terminate-session

Once the leader exits, logind has abandoned the session's scope, and
TerminateSession on such a session does nothing: no error, no journal
line, the session stays closing -- measured on the eight leaked ones,
which it left untouched. kill-session SIGTERM ended every orphan within a
second. The daemon now sends SIGTERM, polls for the session to go for up
to two seconds, then SIGKILLs what is left; polled rather than timed on
a thread, since the next session worker is forked right after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/main.rs | 50 +++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 41 insertions(+), 9 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 888b183..8355b30 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1738,9 +1738,18 @@ fn ensure_vt_active(tty_name: &str) {
 /// the logind session `closing` — with logind's default KillUserProcesses=no,
 /// every process the session started and did not reap lives on in its scope.
 /// Every login leaked that way (by 2026-09-25, eight sessions stuck `closing`,
-/// held open by 16 orphaned 1Password helpers, 1.9 GB). Terminating from the
-/// DAEMON, not the worker: pam_systemd moved the worker into the session's
-/// scope, so it would be terminating itself.
+/// held open by 16 orphaned 1Password helpers, 1.9 GB). Done from the DAEMON,
+/// not the worker: pam_systemd moved the worker into the session's scope.
+///
+/// `kill-session`, NOT `terminate-session`. Once the leader has exited, logind
+/// has abandoned the scope, and TerminateSession on such a session does
+/// nothing at all — no error, no journal line, the session stays `closing`
+/// (measured on the eight leaked ones). Signalling the scope's processes does
+/// work: SIGTERM ended every orphaned helper within a second. SIGKILL follows
+/// for anything still there after two seconds. Escalation is polled here
+/// rather than timed on a thread, because the daemon forks the next session
+/// worker right after this returns, and a thread busy spawning `loginctl` at
+/// that moment is the fork-in-a-threaded-process hazard that wedged logins.
 ///
 /// Also on a compositor-restart relaunch: the new compositor starts its
 /// clients fresh in the new session (nothing survives into it from the old
@@ -1750,16 +1759,39 @@ fn ensure_vt_active(tty_name: &str) {
 fn terminate_session(session_id: &str) {
     if !valid_session_id(session_id) {
         if !session_id.is_empty() {
-            log::warn!("not terminating session with unexpected id {:?}", session_id);
+            log::warn!("not ending session with unexpected id {:?}", session_id);
         }
         return;
     }
-    match std::process::Command::new("loginctl").args(["terminate-session", session_id]).status() {
-        Ok(s) if s.success() => log::info!("Terminated logind session {}", session_id),
-        // Already gone (every process exited on its own) is the good case.
-        Ok(s) => log::info!("loginctl terminate-session {} exited {}", session_id, s),
-        Err(e) => log::warn!("could not run loginctl to end session {}: {}", session_id, e),
+    let session_exists = || {
+        std::process::Command::new("loginctl")
+            .args(["show-session", session_id, "--property=Id"])
+            .stdout(std::process::Stdio::null())
+            .stderr(std::process::Stdio::null())
+            .status()
+            .is_ok_and(|s| s.success())
+    };
+    let kill = |signal: &str| {
+        let _ = std::process::Command::new("loginctl")
+            .args(["kill-session", session_id, "--signal", signal])
+            .stderr(std::process::Stdio::null())
+            .status();
+    };
+    // Nothing left running: logind already dropped it — the good case.
+    if !session_exists() {
+        return;
+    }
+    log::info!("Session {} left processes behind; sending SIGTERM", session_id);
+    kill("SIGTERM");
+    for _ in 0..20 {
+        std::thread::sleep(std::time::Duration::from_millis(100));
+        if !session_exists() {
+            log::info!("Session {} ended", session_id);
+            return;
+        }
     }
+    log::warn!("Session {} still running 2s after SIGTERM; sending SIGKILL", session_id);
+    kill("SIGKILL");
 }
 
 /// Fork the session worker (PAM open_session + user-session spawn) and wait