redbear-sessiond: implement real kill_session, kill_user, power_off, reboot
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user