redbear-sessiond: implement real kill_session, kill_user, power_off, reboot

This commit is contained in:
2026-06-09 10:36:49 +03:00
parent 77bd483327
commit 385f32704a
4 changed files with 273 additions and 13 deletions
@@ -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"
@@ -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);
}
}
@@ -1,6 +1,7 @@
mod acpi_watcher;
mod control;
mod device_map;
mod kernel;
mod manager;
mod runtime_state;
mod seat;
@@ -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(())
}