From 385f32704a3efde964ce7513001ffc7bdfe7ff73 Mon Sep 17 00:00:00 2001 From: Admin Pupkin Date: Tue, 9 Jun 2026 10:36:49 +0300 Subject: [PATCH] redbear-sessiond: implement real kill_session, kill_user, power_off, reboot --- .../system/redbear-sessiond/source/Cargo.toml | 4 +- .../redbear-sessiond/source/src/kernel.rs | 186 ++++++++++++++++++ .../redbear-sessiond/source/src/main.rs | 1 + .../redbear-sessiond/source/src/manager.rs | 95 +++++++-- 4 files changed, 273 insertions(+), 13 deletions(-) create mode 100644 local/recipes/system/redbear-sessiond/source/src/kernel.rs diff --git a/local/recipes/system/redbear-sessiond/source/Cargo.toml b/local/recipes/system/redbear-sessiond/source/Cargo.toml index 525c16584e..d79b2bf288 100644 --- a/local/recipes/system/redbear-sessiond/source/Cargo.toml +++ b/local/recipes/system/redbear-sessiond/source/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "redbear-sessiond" -version = "0.1.0" +version = "0.2.3" edition = "2024" +description = "Red Bear session manager D-Bus daemon v6.0 2026" [[bin]] name = "redbear-sessiond" @@ -14,3 +15,4 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" libredox = "=0.1.16" redox-syscall = { package = "redox_syscall", version = "0.7" } +libc = "0.2" diff --git a/local/recipes/system/redbear-sessiond/source/src/kernel.rs b/local/recipes/system/redbear-sessiond/source/src/kernel.rs new file mode 100644 index 0000000000..1a53f37a1f --- /dev/null +++ b/local/recipes/system/redbear-sessiond/source/src/kernel.rs @@ -0,0 +1,186 @@ +use std::{ + io::{ErrorKind, Write}, + path::Path, + time::{Duration, Instant}, +}; + +use libc::{c_int, pid_t}; + +/// Path to the kernel control file that initiates a power-off (`shutdown` +/// payload) or reboot (`reset` payload). Mirrors the contract used by +/// `recipes/core/coreutils/source/src/bin/shutdown.rs`. +pub const KSTOP_PATH: &str = "/scheme/sys/kstop"; + +/// `SIGTERM` and `SIGKILL` as `c_int` constants. Defined locally to avoid +/// leaking `libc`'s signal-name namespace into other modules. +pub const SIGTERM: c_int = 15; +pub const SIGKILL: c_int = 9; + +/// Grace period between `SIGTERM` and `SIGKILL`, mirroring systemd-logind's +/// default `KillUserProcesses=` interval. +pub const KILL_GRACE: Duration = Duration::from_secs(5); + +#[derive(Debug, PartialEq, Eq)] +pub enum TerminationOutcome { + Stopped, + Escalated, + NotFound, + Failed(c_int), +} + +/// Probe for liveness using `kill(pid, 0)`. `EPERM` is treated as alive — the +/// process is present, the caller just may not signal it. +pub fn process_alive(pid: pid_t) -> bool { + // SAFETY: `kill` is async-signal-safe and takes only scalars. + let result = unsafe { libc::kill(pid, 0) }; + if result == 0 { + return true; + } + let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); + errno != libc::ESRCH +} + +fn send_signal(pid: pid_t, signal: c_int) -> Result<(), TerminationOutcome> { + // SAFETY: `kill` is async-signal-safe. + let result = unsafe { libc::kill(pid, signal) }; + if result == 0 { + return Ok(()); + } + let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); + if errno == libc::ESRCH { + Err(TerminationOutcome::NotFound) + } else { + Err(TerminationOutcome::Failed(errno)) + } +} + +/// Public wrapper used by callers (e.g. the D-Bus `KillSession` handler) that +/// want to deliver a caller-specified signal verbatim, with no escalation. +pub fn send_signal_direct(pid: pid_t, signal: c_int) -> TerminationOutcome { + match send_signal(pid, signal) { + Ok(()) => TerminationOutcome::Stopped, + Err(outcome) => outcome, + } +} + +/// Deliver `SIGTERM` to `pid`, poll liveness for up to `KILL_GRACE`, and +/// escalate to `SIGKILL` if the process is still alive at the deadline. +pub fn terminate_process(pid: pid_t) -> TerminationOutcome { + match send_signal(pid, SIGTERM) { + Ok(()) => {} + Err(TerminationOutcome::NotFound) => return TerminationOutcome::Stopped, + Err(other) => return other, + } + + if !process_alive(pid) { + return TerminationOutcome::Stopped; + } + + let deadline = Instant::now() + KILL_GRACE; + while Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(100)); + if !process_alive(pid) { + return TerminationOutcome::Stopped; + } + } + + match send_signal(pid, SIGKILL) { + Ok(()) => TerminationOutcome::Escalated, + Err(TerminationOutcome::NotFound) => TerminationOutcome::Stopped, + Err(other) => other, + } +} + +/// Deliver `SIGTERM` to the process group identified by `pgid`, then +/// escalate to `SIGKILL` after the grace window. Uses the negated PID +/// convention (`kill(-pgid, sig)`) supported by the Redox kernel. +pub fn terminate_process_group(pgid: pid_t) -> TerminationOutcome { + let negated = pgid.wrapping_neg(); + match send_signal(negated, SIGTERM) { + Ok(()) => {} + Err(TerminationOutcome::NotFound) => return TerminationOutcome::Stopped, + Err(other) => return other, + } + + if !process_alive(negated) { + return TerminationOutcome::Stopped; + } + + let deadline = Instant::now() + KILL_GRACE; + while Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(100)); + if !process_alive(negated) { + return TerminationOutcome::Stopped; + } + } + + match send_signal(negated, SIGKILL) { + Ok(()) => TerminationOutcome::Escalated, + Err(TerminationOutcome::NotFound) => TerminationOutcome::Stopped, + Err(other) => other, + } +} + +/// Write `b"reset"` (when `reboot` is true) or `b"shutdown"` to the kernel +/// `kstop` control file. Returns the underlying I/O error verbatim so callers +/// can log it; the function does not panic on a missing scheme so the daemon +/// stays up and can report the situation to the bus. +pub fn request_kernel_shutdown(reboot: bool) -> std::io::Result<()> { + let payload: &[u8] = if reboot { b"reset" } else { b"shutdown" }; + let path = Path::new(KSTOP_PATH); + let Some(parent) = path.parent() else { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + format!("kstop path {KSTOP_PATH:?} has no parent"), + )); + }; + if !parent.exists() { + return Err(std::io::Error::new( + ErrorKind::NotFound, + format!( + "kernel scheme {parent:?} is not mounted; cannot deliver {payload:?} request" + ), + )); + } + let mut file = std::fs::OpenOptions::new() + .write(true) + .open(path) + .map_err(|err| { + std::io::Error::new( + err.kind(), + format!("failed to open {KSTOP_PATH:?} for shutdown request: {err}"), + ) + })?; + file.write_all(payload).map_err(|err| { + std::io::Error::new( + err.kind(), + format!("failed to write {payload:?} to {KSTOP_PATH:?}: {err}"), + ) + })?; + file.flush().map_err(|err| { + std::io::Error::new( + err.kind(), + format!("failed to flush {payload:?} to {KSTOP_PATH:?}: {err}"), + ) + })?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_kernel_shutdown_returns_io_error_on_missing_scheme() { + let result = request_kernel_shutdown(false); + assert!( + result.is_err(), + "expected I/O error when kstop scheme is absent" + ); + } + + #[test] + fn process_alive_does_not_panic_for_vacant_pid() { + let _ = process_alive(pid_t::MAX - 1); + } +} diff --git a/local/recipes/system/redbear-sessiond/source/src/main.rs b/local/recipes/system/redbear-sessiond/source/src/main.rs index 7f7993318e..26cbfd1c4d 100644 --- a/local/recipes/system/redbear-sessiond/source/src/main.rs +++ b/local/recipes/system/redbear-sessiond/source/src/main.rs @@ -1,6 +1,7 @@ mod acpi_watcher; mod control; mod device_map; +mod kernel; mod manager; mod runtime_state; mod seat; diff --git a/local/recipes/system/redbear-sessiond/source/src/manager.rs b/local/recipes/system/redbear-sessiond/source/src/manager.rs index 9e1ad852f1..5cafade076 100644 --- a/local/recipes/system/redbear-sessiond/source/src/manager.rs +++ b/local/recipes/system/redbear-sessiond/source/src/manager.rs @@ -11,8 +11,36 @@ use zbus::{ zvariant::{OwnedFd, OwnedObjectPath}, }; +use crate::kernel; use crate::runtime_state::{InhibitorEntry, SharedRuntime}; +/// Map the freedesktop.org `KillSession` / `KillUser` arguments into a real +/// signal-delivery invocation. +/// +/// `who == "leader"` targets the single leader PID. `who == "all"` targets the +/// leader's process group, which on Redox is a clean way to reach every +/// descendant spawned by the session. Any other value falls back to the +/// leader, matching the behaviour of systemd-logind. +/// +/// `signal_number == 0` is the freedesktop.org default: deliver `SIGTERM` +/// first, wait the grace window, then escalate to `SIGKILL`. A non-zero +/// `signal_number` is delivered verbatim — callers can request a `SIGINT` for +/// a foregrounded user, for example — with no escalation. +fn dispatch_kill_request( + who: &str, + signal_number: i32, + leader: u32, +) -> kernel::TerminationOutcome { + if signal_number == 0 { + return match who { + "all" => kernel::terminate_process_group(leader as i32), + _ => kernel::terminate_process(leader as i32), + }; + } + + kernel::send_signal_direct(leader as i32, signal_number as i32) +} + #[derive(Clone, Debug)] pub struct LoginManager { runtime: SharedRuntime, @@ -173,12 +201,25 @@ impl LoginManager { if let Ok(mut runtime) = self.runtime.write() { runtime.preparing_for_shutdown = true; } - Ok(()) + kernel::request_kernel_shutdown(false).map_err(|err| { + fdo::Error::Failed(format!( + "power_off: failed to deliver shutdown request to {}: {err}", + kernel::KSTOP_PATH + )) + }) } fn reboot(&self, _interactive: bool) -> fdo::Result<()> { eprintln!("redbear-sessiond: Reboot requested"); - Ok(()) + if let Ok(mut runtime) = self.runtime.write() { + runtime.preparing_for_shutdown = true; + } + kernel::request_kernel_shutdown(true).map_err(|err| { + fdo::Error::Failed(format!( + "reboot: failed to deliver reset request to {}: {err}", + kernel::KSTOP_PATH + )) + }) } fn suspend(&self, _interactive: bool) -> fdo::Result<()> { @@ -320,24 +361,54 @@ impl LoginManager { } fn kill_session(&self, session_id: &str, who: &str, signal_number: i32) -> fdo::Result<()> { - let runtime = self.runtime_read()?; - if !Self::session_matches(&runtime, session_id) { - return Err(fdo::Error::Failed(format!("unknown login1 session '{session_id}'"))); + let (leader, runtime_uid) = { + let runtime = self.runtime_read()?; + if !Self::session_matches(&runtime, session_id) { + return Err(fdo::Error::Failed(format!("unknown login1 session '{session_id}'"))); + } + (runtime.leader, runtime.uid) + }; + + let outcome = dispatch_kill_request(who, signal_number, leader); + eprintln!( + "redbear-sessiond: KillSession({session_id}, who={who}, signal={signal_number}, leader={leader}, uid={runtime_uid}) -> {outcome:?}" + ); + + if matches!(outcome, kernel::TerminationOutcome::Failed(_)) { + return Err(fdo::Error::Failed(format!( + "KillSession({session_id}) failed: {outcome:?}" + ))); } - eprintln!( - "redbear-sessiond: KillSession({session_id}, who={who}, signal={signal_number}) — no-op" - ); + let mut runtime = self.runtime_write()?; + runtime.state = String::from("closing"); + runtime.active = false; Ok(()) } fn kill_user(&self, uid: u32, signal_number: i32) -> fdo::Result<()> { - let runtime = self.runtime_read()?; - if !Self::user_matches(&runtime, uid) { - return Err(fdo::Error::Failed(format!("unknown login1 user uid {uid}"))); + let (leader, session_id) = { + let runtime = self.runtime_read()?; + if !Self::user_matches(&runtime, uid) { + return Err(fdo::Error::Failed(format!("unknown login1 user uid {uid}"))); + } + (runtime.leader, runtime.session_id.clone()) + }; + + let outcome = dispatch_kill_request("user", signal_number, leader); + eprintln!( + "redbear-sessiond: KillUser({uid}, signal={signal_number}, session={session_id}, leader={leader}) -> {outcome:?}" + ); + + if matches!(outcome, kernel::TerminationOutcome::Failed(_)) { + return Err(fdo::Error::Failed(format!( + "KillUser({uid}) failed: {outcome:?}" + ))); } - eprintln!("redbear-sessiond: KillUser({uid}, signal={signal_number}) — no-op"); + let mut runtime = self.runtime_write()?; + runtime.state = String::from("closing"); + runtime.active = false; Ok(()) }