diff --git a/local/docs/SUPERSEDED-DOC-LOG.md b/local/docs/SUPERSEDED-DOC-LOG.md index e2e5c13b37..c3809b5190 100644 --- a/local/docs/SUPERSEDED-DOC-LOG.md +++ b/local/docs/SUPERSEDED-DOC-LOG.md @@ -188,6 +188,12 @@ with the deletion log. | 14 | `local/recipes/system/firmware-loader` daemon 9-`.expect()` chain | (this commit) | All converted to `Result` propagation with log+exit | | 14 | `redbear-{keymapd,ime,accessibility}` scheme daemon `.expect()` | (this commit) | Converted to `Result` propagation with `log_msg("ERROR", …)` + `process::exit(1)` | | 14 | `local/recipes/AGENTS.md` redbear-wayland-guard entry marked "Rust" | (this commit) | Struck through — was actually a C LD_PRELOAD stub | +| 15 | `local/recipes/system/redbear-greeter/main.rs` `kill_child()` | `5682072e58` | `let _ = process.kill()/wait()` → logs kill+wait failure (zombie-child prevention) | +| 15 | `local/recipes/system/redbear-netctl/main.rs` (lines 201, 224) | `5682072e58` | `let _ = fs::remove_file()` → logs failure (stale active-profile prevention) | +| 15 | `local/recipes/system/redbear-usb-hotplugd/main.rs:254` | `5682072e58` | `let _ = child.kill()` → logs kill failure (USB-device-daemon leak prevention) | +| 15 | `local/recipes/system/redbear-acmd/main.rs:221` | `883e8147ec` | **SECURITY** — bare `let _ = setrens(0, 0)` → logs and returns on failure (privilege-boundary defense) | +| 15 | `README.md` status table null+8 contradiction | `883e8147ec` | Line 138 demoted ✅ → 🟡 with caveat about runtime re-verification pending; four lines now agree | +| 15 | `README.md` + `AGENTS.md` `redbear-live.iso` stale | `883e8147ec` | Replaced with accurate `.iso` pattern (no `redbear-live` config exists) | **Pattern emerging:** Lie-grade code in Red Bear is concentrated in three places — (a) relibc fork panic-site catch-alls, (b) scheme daemon diff --git a/local/recipes/system/redbear-sessiond/source/src/manager.rs b/local/recipes/system/redbear-sessiond/source/src/manager.rs index ae15179934..d46427d429 100644 --- a/local/recipes/system/redbear-sessiond/source/src/manager.rs +++ b/local/recipes/system/redbear-sessiond/source/src/manager.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashSet, fs, io::Write, os::fd::OwnedFd as StdOwnedFd, @@ -7,12 +8,15 @@ use std::{ Arc, Mutex, atomic::{AtomicBool, Ordering}, }, + time::Duration, }; use zbus::{ Connection, - fdo, + fdo::{self, DBusProxy}, interface, + message::Header, + names::BusName, object_server::SignalEmitter, zvariant::{OwnedFd, OwnedObjectPath}, }; @@ -21,15 +25,18 @@ use tokio::spawn as tokio_spawn; use crate::runtime_state::{InhibitorEntry, SharedRuntime}; +type InhibitorFdSlot = (Option, StdOwnedFd); + #[derive(Clone, Debug)] pub struct LoginManager { runtime: SharedRuntime, session_path: OwnedObjectPath, seat_path: OwnedObjectPath, user_path: OwnedObjectPath, - inhibitor_fds: Arc>>, + inhibitor_fds: Arc>>, connection: Arc>>, seat_announced: Arc, + dead_senders: Arc>>, } impl LoginManager { @@ -47,6 +54,7 @@ impl LoginManager { inhibitor_fds: Arc::new(Mutex::new(Vec::new())), connection: Arc::new(Mutex::new(None)), seat_announced: Arc::new(AtomicBool::new(false)), + dead_senders: Arc::new(Mutex::new(HashSet::new())), } } @@ -56,6 +64,9 @@ impl LoginManager { } let self_clone = self.clone(); tokio_spawn(async move { self_clone.announce_seat_if_needed().await }); + + let self_clone = self.clone(); + tokio_spawn(async move { self_clone.run_inhibitor_reaper().await }); } pub async fn announce_seat_if_needed(&self) { @@ -76,6 +87,99 @@ impl LoginManager { .and_then(|g| g.as_ref().cloned()) } + /// Remove all inhibitors whose `sender` matches `vanished_sender`, + /// and drop the corresponding daemon-side pipe FDs so the pipe + /// breaks and the caller (if still alive) observes EOF. + pub fn reap_inhibitors_for_sender(&self, vanished_sender: &str) { + if let Ok(mut dead) = self.dead_senders.lock() { + dead.insert(vanaged_sender.to_owned()); + } + let mut removed = 0usize; + if let Ok(mut runtime) = self.runtime.write() { + let before = runtime.inhibitors.len(); + runtime.inhibitors + .retain(|e| e.sender.as_deref() != Some(vanished_sender)); + removed = before - runtime.inhibitors.len(); + } + if let Ok(mut fds) = self.inhibitor_fds.lock() { + fds.retain(|(s, _)| s.as_deref() != Some(vanished_sender)); + } + if removed > 0 { + eprintln!( + "redbear-sessiond: reaped {removed} inhibitor(s) for vanished bus name '{vanished_sender}'" + ); + } + } + + /// Background task: periodically asks the D-Bus daemon whether each + /// tracked sender still owns its unique name. When `name_has_owner` + /// returns `false`, the sender has disconnected and its inhibitors + /// are reaped. + /// + /// Uses `org.freedesktop.DBus.NameHasOwner` polling instead of a + /// `NameOwnerChanged` signal subscription because consuming zbus + /// signal streams requires a `StreamExt` that is not available + /// without adding a direct dependency on `futures-lite` / + /// `futures-util`. The 2-second poll interval keeps reaping + /// responsive while staying lightweight. + pub async fn run_inhibitor_reaper(&self) { + let conn = match self.get_connection() { + Some(c) => c, + None => { + eprintln!( + "redbear-sessiond: inhibitor reaper has no connection; inhibitor lifecycle reaping disabled" + ); + return; + } + }; + + let proxy = match DBusProxy::new(&conn).await { + Ok(p) => p, + Err(err) => { + eprintln!( + "redbear-sessiond: inhibitor reaper failed to create DBus proxy: {err}" + ); + return; + } + }; + + let mut interval = tokio::time::interval(Duration::from_secs(2)); + interval.tick().await; + + loop { + interval.tick().await; + + let senders: Vec = match self.runtime.read() { + Ok(runtime) => runtime + .inhibitors + .iter() + .filter_map(|e| e.sender.clone()) + .collect::>() + .into_iter() + .collect(), + Err(_) => continue, + }; + + for sender in &senders { + let name = match BusName::try_from(sender.as_str()) { + Ok(n) => n, + Err(_) => continue, + }; + match proxy.name_has_owner(name).await { + Ok(true) => {} + Ok(false) => { + self.reap_inhibitors_for_sender(sender); + } + Err(err) => { + eprintln!( + "redbear-sessiond: inhibitor reaper name_has_owner('{sender}') failed: {err}" + ); + } + } + } + } + } + async fn emit_prepare_for_shutdown(&self, before: bool) { if let Some(conn) = self.get_connection() { if let Err(err) = conn @@ -288,6 +392,10 @@ impl LoginManager { } } + // Hardcoded "na" until /scheme/sys/sleep + /scheme/sys/hybrid_sleep + // land; when they do, mirror the kstop_writable() probe pattern + // used by can_power_off/can_reboot/can_suspend so the functions + // auto-enable. fn can_hibernate(&self) -> fdo::Result { Ok(String::from("na")) } @@ -758,9 +866,15 @@ mod tests { #[test] fn can_methods_return_na() { let manager = test_manager(); - assert_eq!(manager.can_power_off().unwrap(), "yes"); - assert_eq!(manager.can_reboot().unwrap(), "yes"); - assert_eq!(manager.can_suspend().unwrap(), "yes"); + // kstop_writable() probes /scheme/sys/kstop, which only exists on + // the Redox target. On a Linux host (where `cargo test` runs for + // host-runnable unit tests), the path is absent so the Can* + // methods correctly return "na". Mirror that runtime detection + // here so the test passes on both environments. + let expected = if kstop_writable() { "yes" } else { "na" }; + assert_eq!(manager.can_power_off().unwrap(), expected); + assert_eq!(manager.can_reboot().unwrap(), expected); + assert_eq!(manager.can_suspend().unwrap(), expected); assert_eq!(manager.can_hibernate().unwrap(), "na"); assert_eq!(manager.can_hybrid_sleep().unwrap(), "na"); assert_eq!(manager.can_suspend_then_hibernate().unwrap(), "na");