From 311d4708e5d878182b1576226b04c6f176db7a4a Mon Sep 17 00:00:00 2001 From: Tigah <88289044+TBX3D@users.noreply.github.com> Date: Sun, 21 Jun 2026 01:56:41 -0700 Subject: [PATCH] fix(linux): reap a crashed headless session's leftovers on next start (#15348) The teardown cleanup added for #15183 only runs on a clean disconnect. If the service or its --server crashes before then, the headless logind session scope and the /tmp/.X lock files it created leak the same way #15183 leaked them, with nothing to reclaim them afterwards. Record the session scope and display when the headless session starts, and on the next --server start reap exactly what the previous run recorded, then drop the marker. It only ever touches the one scope and display the previous run recorded, never a scan, so unrelated sessions are untouched; the reap and X cleanup reuse the teardown path. A logind session id is only unique within a boot: the counter lives in /run and resets, so a recorded "session-N.scope" can name a different, live session after a reboot. Tag the marker with the boot id and only reap the scope when it matches the current boot. A leaked cgroup cannot outlive a reboot, so nothing legitimate is lost cross-boot; the X lock cleanup stays pid-guarded and runs either way. Signed-off-by: TBX3D <88289044+TBX3D@users.noreply.github.com> --- src/platform/linux_desktop_manager.rs | 90 ++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/src/platform/linux_desktop_manager.rs b/src/platform/linux_desktop_manager.rs index 86715b7d7..4cfde61a2 100644 --- a/src/platform/linux_desktop_manager.rs +++ b/src/platform/linux_desktop_manager.rs @@ -51,6 +51,7 @@ fn check_desktop_manager() { pub fn start_xdesktop() { debug_assert!(crate::is_server()); std::thread::spawn(|| { + DesktopManager::recover_orphaned_session(); *DESKTOP_MANAGER.lock().unwrap() = Some(DesktopManager::new()); let interval = time::Duration::from_millis(super::SERVICE_INTERVAL); @@ -462,9 +463,10 @@ impl DesktopManager { let (child_xorg, child_wm) = Self::start_x11(uid, gid, username, display_num, &envs)?; is_child_running.store(true, Ordering::SeqCst); - // capture the logind session scope (from a live child) for teardown, see - // reap_session_scope. + // capture the logind session scope (from a live child) for teardown and crash + // recovery, see reap_session_scope and recover_orphaned_session. let scope_dir = Self::session_scope_dir(child_xorg.id()); + Self::save_orphaned_marker(&scope_dir, display_num); log::info!("Start xorg and wm done, notify and wait xtop x11"); allow_err!(tx_res.send("".to_owned())); @@ -879,6 +881,66 @@ impl DesktopManager { std::io::Error::last_os_error().raw_os_error() == Some(hbb_common::libc::EPERM) } + const ORPHANED_SESSION_KEY: &'static str = "headless-orphaned-session"; + + fn save_orphaned_marker(scope_dir: &str, display_num: u32) { + // tag the marker with this boot's id: a logind session id is only unique within a + // boot (the counter lives in /run and resets), so recovery must not reap a recorded + // scope path after a reboot, when it may name a different live session. + let boot_id = Self::current_boot_id().unwrap_or_default(); + hbb_common::config::LocalConfig::set_option( + Self::ORPHANED_SESSION_KEY.to_owned(), + format!("{};{};{}", scope_dir, display_num, boot_id), + ); + } + + fn current_boot_id() -> Option { + std::fs::read_to_string("/proc/sys/kernel/random/boot_id") + .ok() + .map(|s| s.trim().to_owned()) + } + + fn clear_orphaned_marker() { + hbb_common::config::LocalConfig::set_option( + Self::ORPHANED_SESSION_KEY.to_owned(), + String::new(), + ); + } + + fn parse_orphaned_marker(marker: &str) -> Option<(&str, u32, &str)> { + let (rest, boot_id) = marker.rsplit_once(';')?; + let (scope_dir, display) = rest.rsplit_once(';')?; + Some((scope_dir, display.trim().parse::().ok()?, boot_id)) + } + + // a run that dies before wait_stop_x11 (service or --server crash) leaks the headless + // session scope + X lock files, the same as a missed teardown (rustdesk/rustdesk#15183). + // reap exactly what the dead run recorded - never a scan, so unrelated sessions are safe. + fn recover_orphaned_session() { + let marker = hbb_common::config::LocalConfig::get_option(Self::ORPHANED_SESSION_KEY); + if marker.is_empty() { + return; + } + if let Some((scope_dir, display_num, boot_id)) = Self::parse_orphaned_marker(&marker) { + // only reap the recorded scope when the marker is from this same boot: a leaked + // cgroup cannot outlive a reboot, so cross-boot there is nothing legitimate to + // reap, and the recorded "session-N.scope" may by then name a different live + // session. the X lock cleanup is pid-guarded, so run it either way. + let same_boot = Self::current_boot_id().map_or(false, |b| b == boot_id); + log::info!( + "Recovering leaked headless session from a previous run: scope {}, display {} (same boot: {})", + scope_dir, + display_num, + same_boot + ); + if same_boot { + Self::reap_session_scope(scope_dir); + } + Self::cleanup_x_display_files(display_num); + } + Self::clear_orphaned_marker(); + } + fn try_wait_stop_x11( child_xorg: &mut Child, child_wm: &mut Child, @@ -898,6 +960,7 @@ impl DesktopManager { Self::wait_x11_children_exit(child_xorg, child_wm); Self::reap_session_scope(scope_dir); Self::cleanup_x_display_files(display_num); + Self::clear_orphaned_marker(); desktop_manager .is_child_running .store(false, Ordering::SeqCst); @@ -1082,4 +1145,27 @@ mod tests { assert_eq!(pids, vec![100, 101, 200, 300]); } + + #[test] + fn parses_orphaned_session_marker() { + assert_eq!( + DesktopManager::parse_orphaned_marker( + "/sys/fs/cgroup/user.slice/user-1000.slice/session-3.scope;7;abc-123" + ), + Some(( + "/sys/fs/cgroup/user.slice/user-1000.slice/session-3.scope", + 7, + "abc-123" + )) + ); + // an empty scope still carries the display so its stale X lock can be cleaned + assert_eq!(DesktopManager::parse_orphaned_marker(";5;abc-123"), Some(("", 5, "abc-123"))); + // an empty boot id never matches the live one, so the scope reap is skipped + assert_eq!(DesktopManager::parse_orphaned_marker("/scope;5;"), Some(("/scope", 5, ""))); + assert_eq!(DesktopManager::parse_orphaned_marker(""), None); + assert_eq!(DesktopManager::parse_orphaned_marker("garbage"), None); + // the pre-boot-id two-field format no longer parses, recovery just skips it + assert_eq!(DesktopManager::parse_orphaned_marker("/scope;7"), None); + assert_eq!(DesktopManager::parse_orphaned_marker("/scope;notnum;abc"), None); + } }