diff --git a/config/protected-recipes.toml b/config/protected-recipes.toml index 4ae46531ff..cd35177cbf 100644 --- a/config/protected-recipes.toml +++ b/config/protected-recipes.toml @@ -63,7 +63,8 @@ recipes = [ # Red Bear custom libs (no stubs) [libs] recipes = [ - "libudev", "zbus", "libqrencode", + "zbus", "libqrencode", + "pipewire", "wireplumber", ] # Red Bear Wayland diff --git a/config/redbear-full.toml b/config/redbear-full.toml index 86512047a1..30f543e901 100644 --- a/config/redbear-full.toml +++ b/config/redbear-full.toml @@ -84,6 +84,16 @@ redbear-keymapd = {} redbear-ime = {} redbear-accessibility = {} +# Audio — PipeWire core + WirePlumber session manager. +# These provide the audio backend for KDE Plasma (Phonon, KMix, +# the Plasma audio widget) via the standard freedesktop.org +# org.freedesktop.PipeWire / org.pulseaudio.Server D-Bus surface. +# The audiod scheme daemon (in the base package) is the underlying +# audio device; pipewire and wireplumber run as the policy and +# session layer above it. +pipewire = {} +wireplumber = {} + # Qt6 stack qtbase = {} qtdeclarative = {} @@ -352,6 +362,36 @@ type = "oneshot_async" path = "/etc/keymaps/.gitkeep" data = "" +[[files]] +path = "/etc/init.d/15_pipewire.service" +data = """ +[unit] +description = "PipeWire multimedia server (graph core, PulseAudio compat shim, libpipewire)" +requires_weak = [ + "12_dbus.service", + "13_redbear-sessiond.service", +] + +[service] +cmd = "/usr/bin/pipewire" +type = "oneshot_async" +""" + +[[files]] +path = "/etc/init.d/16_wireplumber.service" +data = """ +[unit] +description = "WirePlumber PipeWire session and policy manager" +requires_weak = [ + "12_dbus.service", + "15_pipewire.service", +] + +[service] +cmd = "/usr/bin/wireplumber" +type = "oneshot_async" +""" + [[files]] path = "/etc/init.d/13_redbear-keymapd.service" data = """ diff --git a/local/recipes/system/redbear-dbus-services/files/session-services/org.freedesktop.impl.pulseaudio.service b/local/recipes/system/redbear-dbus-services/files/session-services/org.freedesktop.impl.pulseaudio.service new file mode 100644 index 0000000000..195939b9d2 --- /dev/null +++ b/local/recipes/system/redbear-dbus-services/files/session-services/org.freedesktop.impl.pulseaudio.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=org.freedesktop.impl.pulseaudio +Exec=/usr/bin/pipewire-pulse +User=root +SystemdService=pipewire-pulse.service diff --git a/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.PipeWire.service b/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.PipeWire.service new file mode 100644 index 0000000000..6640a19392 --- /dev/null +++ b/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.PipeWire.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=org.freedesktop.PipeWire +Exec=/usr/bin/pipewire +User=root +SystemdService=pipewire.service diff --git a/local/recipes/system/redbear-dbus-services/files/system-services/org.pulseaudio.Server.service b/local/recipes/system/redbear-dbus-services/files/system-services/org.pulseaudio.Server.service new file mode 100644 index 0000000000..6c9e103645 --- /dev/null +++ b/local/recipes/system/redbear-dbus-services/files/system-services/org.pulseaudio.Server.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=org.pulseaudio.Server +Exec=/usr/bin/pipewire-pulse +User=root +SystemdService=pipewire-pulse.service diff --git a/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.PipeWire.conf b/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.PipeWire.conf new file mode 100644 index 0000000000..88cd999da5 --- /dev/null +++ b/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.PipeWire.conf @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/system/redbear-dbus-services/files/system.d/org.pulseaudio.Server.conf b/local/recipes/system/redbear-dbus-services/files/system.d/org.pulseaudio.Server.conf new file mode 100644 index 0000000000..9974e5899c --- /dev/null +++ b/local/recipes/system/redbear-dbus-services/files/system.d/org.pulseaudio.Server.conf @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/local/recipes/system/redbear-sessiond/source/Cargo.toml b/local/recipes/system/redbear-sessiond/source/Cargo.toml index d79b2bf288..72b849ba67 100644 --- a/local/recipes/system/redbear-sessiond/source/Cargo.toml +++ b/local/recipes/system/redbear-sessiond/source/Cargo.toml @@ -2,7 +2,7 @@ name = "redbear-sessiond" version = "0.2.3" edition = "2024" -description = "Red Bear session manager D-Bus daemon v6.0 2026" +description = "Red Bear session manager D-Bus daemon 0.2.3 2026" [[bin]] name = "redbear-sessiond" diff --git a/local/recipes/system/redbear-sessiond/source/src/apply_groups.rs b/local/recipes/system/redbear-sessiond/source/src/apply_groups.rs new file mode 100644 index 0000000000..3d8303cbfc --- /dev/null +++ b/local/recipes/system/redbear-sessiond/source/src/apply_groups.rs @@ -0,0 +1,152 @@ +//! Supplementary-groups setter for spawned user sessions. +//! +//! The Redox kernel added a `groups: Vec` field to its `Context` +//! struct and exposes it through the proc scheme at +//! `/scheme/proc/self/auth-N-groups`, where `N` is the scheme-handle +//! number of the proc-scheme fd that the kernel hands every process at +//! exec time (see `local/sources/kernel/src/scheme/proc.rs`, +//! `ContextHandle::Groups`). Writing a packed `u32` list to that fd +//! replaces the supplementary groups of the current context and every +//! context that shares its `owner_proc_id` — the new value propagates to +//! children the process has spawned or will spawn. +//! +//! Before the 2026-04-30 kernel change, the session-launch helper had a +//! `cfg(target_os = "redox")` no-op for `apply_groups`. That is a stale- +//! code bug: a `setgroups()` call after `setuid()` would silently leave +//! the process in a state where the kernel's group table is empty even +//! though `getgroups()` reads a non-empty list from `/etc/group`. +//! Permission checks that consult the kernel (file access) would then +//! behave differently from userspace queries. +//! +//! The `cfg(target_os = "redox")` arm is the canonical implementation. +//! The `not(target_os = "redox")` arm forwards to `libc::setgroups` so the +//! same code links into a host-side test binary. + +use std::{fs::OpenOptions, io, io::Write}; + +/// Set the supplementary groups of the current process to `groups`. +/// +/// On Redox the list is written to the kernel's proc-scheme `groups` +/// handle. On every other target the call is forwarded to +/// `libc::setgroups`. An empty `groups` is a no-op on every platform. +#[allow(dead_code)] +pub fn apply_groups(groups: &[u32]) -> io::Result<()> { + if groups.is_empty() { + return Ok(()); + } + + #[cfg(target_os = "redox")] + { + apply_groups_redox(groups) + } + + #[cfg(not(target_os = "redox"))] + { + apply_groups_libc(groups) + } +} + +#[cfg(target_os = "redox")] +#[allow(dead_code)] +fn apply_groups_redox(groups: &[u32]) -> io::Result<()> { + let mut handle = open_groups_handle()?; + let mut buf = Vec::with_capacity(groups.len() * std::mem::size_of::()); + for gid in groups { + buf.extend_from_slice(&gid.to_ne_bytes()); + } + handle.write_all(&buf).map_err(|err| { + io::Error::new( + err.kind(), + format!("apply_groups: write to kernel proc groups handle failed: {err}"), + ) + })?; + Ok(()) +} + +#[cfg(not(target_os = "redox"))] +fn apply_groups_libc(groups: &[u32]) -> io::Result<()> { + let mut native: Vec = groups.iter().map(|&g| g as libc::gid_t).collect(); + let result = unsafe { + libc::setgroups(native.len() as libc::size_t, native.as_mut_ptr()) + }; + if result != 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +/// Open the kernel proc-scheme "groups" handle for the current process. +/// +/// The proc-scheme path is `/scheme/proc/self/auth-N-groups`, where `N` +/// is the scheme-handle number of the proc-scheme fd. The first line of +/// `/scheme/proc/self/status` is `::`; we parse it to +/// recover the handle number. +#[cfg(target_os = "redox")] +#[allow(dead_code)] +fn open_groups_handle() -> io::Result { + use std::io::Read; + + let mut status = std::fs::File::open("/scheme/proc/self/status").map_err(|err| { + io::Error::new( + err.kind(), + format!("apply_groups: cannot open /scheme/proc/self/status: {err}"), + ) + })?; + let mut buf = String::new(); + status.read_to_string(&mut buf).map_err(|err| { + io::Error::new( + err.kind(), + format!("apply_groups: failed to read proc status: {err}"), + ) + })?; + + let first_line = buf.lines().next().ok_or_else(|| { + io::Error::new( + io::ErrorKind::UnexpectedEof, + "apply_groups: proc status is empty", + ) + })?; + let mut fields = first_line.split(':'); + let _pid = fields.next(); + let handle_str = fields.next().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "apply_groups: proc status missing handle field", + ) + })?; + let handle: usize = handle_str.parse().map_err(|err| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("apply_groups: handle field is not a usize: {err}"), + ) + })?; + + let path = format!("/scheme/proc/self/auth-{handle}-groups"); + OpenOptions::new() + .write(true) + .open(&path) + .map_err(|err| { + io::Error::new( + err.kind(), + format!("apply_groups: cannot open {path}: {err}"), + ) + }) +} + +#[cfg(test)] +mod tests { + use super::apply_groups; + + #[test] + fn empty_groups_succeeds() { + apply_groups(&[]).expect("empty group list should be a no-op success"); + } + + #[test] + fn gids_cast_through_libc_on_host() { + let groups: [u32; 3] = [0, 1000, 1001]; + for &g in &groups { + assert_eq!(g as libc::gid_t as u32, g, "gid {g} lost precision"); + } + } +} diff --git a/local/recipes/system/redbear-sessiond/source/src/caller_credentials.rs b/local/recipes/system/redbear-sessiond/source/src/caller_credentials.rs new file mode 100644 index 0000000000..41c624523d --- /dev/null +++ b/local/recipes/system/redbear-sessiond/source/src/caller_credentials.rs @@ -0,0 +1,60 @@ +//! D-Bus caller-credential extraction for privileged methods. +//! +//! The `org.freedesktop.login1` methods exposed by `redbear-sessiond` (power +//! actions, session termination, inhibitors, kill requests) must be authorized +//! against the actual D-Bus caller UID — not the runtime UID of the logged-in +//! user. The bus daemon assigns each connection a UID at authentication time +//! and exposes it via `org.freedesktop.DBus.GetConnectionUnixUser(unique_name)`. +//! +//! In zbus 5 the dispatch passes the message header and connection into the +//! interface method body via the `#[zbus(header)]` and `#[zbus(connection)]` +//! attributes, so this lookup is a direct, single round trip to the bus. + +use zbus::{ + fdo::DBusProxy, + message::Header, + names::BusName, + Connection, + Error as ZbusError, +}; + +/// Look up the caller's OS-level UID via the bus daemon. +/// +/// The UID is stable for the lifetime of the connection and cannot be forged +/// by the caller. Returns `Err(ZbusError::MissingField)` when the message +/// has no `sender` (e.g. signals from the bus itself). +pub async fn caller_uid(connection: &Connection, header: Header<'_>) -> Result { + let sender = header.sender().ok_or(ZbusError::MissingField)?.clone(); + let bus_name: BusName<'_> = sender.into(); + + let dbus = DBusProxy::new(connection).await?; + Ok(dbus.get_connection_unix_user(bus_name).await?) +} + +/// Return `true` when the caller may act on the runtime session. +/// +/// Root is always allowed. The runtime user is allowed (so a session can +/// terminate itself). All other UIDs are rejected; broader policy is the +/// responsibility of `redbear-polkit` upstream of this call. +#[allow(dead_code)] +pub async fn caller_is_authorized( + connection: &Connection, + header: Header<'_>, + runtime_uid: u32, +) -> Result { + let uid = caller_uid(connection, header).await?; + Ok(uid == 0 || uid == runtime_uid) +} + +#[cfg(test)] +mod tests { + #[test] + fn authorization_decision_is_pure() { + let decide = |caller: u32, runtime: u32| caller == 0 || caller == runtime; + + assert!(decide(0, 1000)); + assert!(decide(1000, 1000)); + assert!(!decide(1001, 1000)); + assert!(decide(0, 0)); + } +} diff --git a/local/recipes/system/redbear-sessiond/source/src/device_map.rs b/local/recipes/system/redbear-sessiond/source/src/device_map.rs index 2bf57a5940..f03541c7a2 100644 --- a/local/recipes/system/redbear-sessiond/source/src/device_map.rs +++ b/local/recipes/system/redbear-sessiond/source/src/device_map.rs @@ -5,9 +5,6 @@ use std::{ path::{Path, PathBuf}, }; -#[cfg(unix)] -use std::os::unix::fs::MetadataExt; - #[derive(Clone, Debug)] pub struct DeviceMap { static_paths: HashMap<(u32, u32), String>, @@ -33,10 +30,10 @@ impl DeviceMap { } /// Build a device map that merges static entries with dynamically discovered - /// devices by scanning `/scheme/drm/card*` and `/dev/input/event*` at startup. - /// For each discovered path, stat is used to read the rdev (device number). - /// Entries with a nonzero rdev are inserted into the map; static entries are - /// kept as fallback when rdev is unavailable or zero. + /// devices by scanning `/scheme/drm/card*` and `/dev/input/event*` at + /// startup. Redox schemes assign their own device identifiers, so the + /// (major, minor) tuple is assigned from the well-known path layout, not + /// from `rdev()`. pub fn discover() -> Self { let mut paths = HashMap::from([ ((226, 0), String::from("/scheme/drm/card0")), @@ -109,8 +106,8 @@ impl DeviceMap { } } -/// Scan `/scheme/drm/` for `card*` entries and merge any with a nonzero rdev -/// into the provided map. Static entries are not overwritten. +/// Scan `/scheme/drm/` for `card*` entries and merge any whose name parses +/// cleanly into the map. Static entries are not overwritten. fn discover_scheme_drm(paths: &mut HashMap<(u32, u32), String>) { let entries = match fs::read_dir("/scheme/drm") { Ok(entries) => entries, @@ -125,26 +122,18 @@ fn discover_scheme_drm(paths: &mut HashMap<(u32, u32), String>) { if !name.starts_with("card") { continue; } - - #[cfg(unix)] - if let Ok(metadata) = fs::metadata(&path) { - let rdev = metadata.rdev(); - if rdev != 0 { - let major = dev_major(rdev); - let minor = dev_minor(rdev); - paths - .entry((major, minor)) - .or_insert_with(|| path.to_string_lossy().into_owned()); - } - } - - #[cfg(not(unix))] - let _ = &path; + let Some(minor) = parse_trailing_number(name, "card") else { + continue; + }; + let key = (226_u32, minor); + paths + .entry(key) + .or_insert_with(|| path.to_string_lossy().into_owned()); } } -/// Scan `/dev/input/` for `event*` entries and merge any with a nonzero rdev -/// into the provided map. Static entries are not overwritten. +/// Scan `/dev/input/` for `event*` entries and merge any whose name parses +/// cleanly into the map. Static entries are not overwritten. fn discover_dev_input(paths: &mut HashMap<(u32, u32), String>) { let entries = match fs::read_dir("/dev/input") { Ok(entries) => entries, @@ -159,21 +148,13 @@ fn discover_dev_input(paths: &mut HashMap<(u32, u32), String>) { if !name.starts_with("event") { continue; } - - #[cfg(unix)] - if let Ok(metadata) = fs::metadata(&path) { - let rdev = metadata.rdev(); - if rdev != 0 { - let major = dev_major(rdev); - let minor = dev_minor(rdev); - paths - .entry((major, minor)) - .or_insert_with(|| path.to_string_lossy().into_owned()); - } - } - - #[cfg(not(unix))] - let _ = &path; + let Some(index) = parse_trailing_number(name, "event") else { + continue; + }; + let key = (13_u32, 64_u32 + index); + paths + .entry(key) + .or_insert_with(|| path.to_string_lossy().into_owned()); } } @@ -213,48 +194,93 @@ fn read_dir_paths(dir: &str, include: impl Fn(&str) -> bool) -> Vec { paths } -#[cfg(unix)] +/// Match `path` against the (DRM, INPUT, FB) major numbers used by the +/// freedesktop.org login1 API. Redox `rdev()` does not implement the Linux +/// dev_t encoding, so identification is path-based. fn path_matches_device(path: &Path, major: u32, minor: u32) -> bool { - let Ok(metadata) = fs::metadata(path) else { + let Some(name) = path.file_name().and_then(|n| n.to_str()) else { return false; }; - let rdev = metadata.rdev(); - dev_major(rdev) == major && dev_minor(rdev) == minor -} + let parent = path.parent().and_then(|p| p.to_str()).unwrap_or(""); -#[cfg(not(unix))] -fn path_matches_device(_path: &Path, _major: u32, _minor: u32) -> bool { + if name.starts_with("card") && (parent == "/scheme/drm" || parent == "/dev/dri") { + if let Some(index) = parse_trailing_number(name, "card") { + return major == 226 && minor == index; + } + } + if name.starts_with("event") && (parent == "/dev/input" || parent == "/scheme/input") { + if let Some(index) = parse_trailing_number(name, "event") { + return major == 13 && minor == 64 + index; + } + } + if (name == "fb0" || name == "fb1") && (parent == "/dev" || parent == "/scheme/fb") { + let index: u32 = name.trim_start_matches("fb").parse().unwrap_or(0); + return major == 29 && minor == index; + } false } -fn dev_major(device: u64) -> u32 { - (((device >> 31 >> 1) & 0xfffff000) | ((device >> 8) & 0x00000fff)) as u32 -} - -fn dev_minor(device: u64) -> u32 { - (((device >> 12) & 0xffffff00) | (device & 0x000000ff)) as u32 +/// Parse the trailing decimal digits of `name` after `prefix` and return them +/// as a `u32`. Returns `None` if the prefix is missing, the digits do not fit, +/// or there are no digits. +fn parse_trailing_number(name: &str, prefix: &str) -> Option { + let suffix = name.strip_prefix(prefix)?; + if suffix.is_empty() { + return None; + } + suffix.parse::().ok() } #[cfg(test)] mod tests { - use super::{dev_major, dev_minor}; + use super::{path_matches_device, parse_trailing_number}; - fn make_dev(major: u64, minor: u64) -> u64 { - ((major & 0xfffff000) << 32) - | ((major & 0x00000fff) << 8) - | ((minor & 0xffffff00) << 12) - | (minor & 0x000000ff) + #[test] + fn parses_trailing_decimal() { + assert_eq!(parse_trailing_number("card0", "card"), Some(0)); + assert_eq!(parse_trailing_number("card12", "card"), Some(12)); + assert_eq!(parse_trailing_number("event3", "event"), Some(3)); + assert_eq!(parse_trailing_number("event64", "event"), Some(64)); + assert_eq!(parse_trailing_number("card", "card"), None); + assert_eq!(parse_trailing_number("card-1", "card"), None); + assert_eq!(parse_trailing_number("cardX", "card"), None); } #[test] - fn splits_compound_dev_numbers() { - let device = make_dev(226, 3); - assert_eq!(dev_major(device), 226); - assert_eq!(dev_minor(device), 3); + fn path_matches_drm_card() { + let path = std::path::Path::new("/scheme/drm/card0"); + assert!(path_matches_device(path, 226, 0)); + assert!(path_matches_device(path, 226, 1) == false); - let event = make_dev(13, 67); - assert_eq!(dev_major(event), 13); - assert_eq!(dev_minor(event), 67); + let path2 = std::path::Path::new("/dev/dri/card2"); + assert!(path_matches_device(path2, 226, 2)); + } + + #[test] + fn path_matches_input_event() { + let path = std::path::Path::new("/dev/input/event0"); + assert!(path_matches_device(path, 13, 64)); + assert!(path_matches_device(path, 13, 65) == false); + + let path2 = std::path::Path::new("/scheme/input/event3"); + assert!(path_matches_device(path2, 13, 67)); + } + + #[test] + fn path_matches_framebuffer() { + let path = std::path::Path::new("/dev/fb0"); + assert!(path_matches_device(path, 29, 0)); + + let path2 = std::path::Path::new("/scheme/fb/fb1"); + assert!(path_matches_device(path2, 29, 1)); + } + + #[test] + fn path_rejects_unrelated() { + let path = std::path::Path::new("/scheme/null"); + assert!(!path_matches_device(path, 226, 0)); + assert!(!path_matches_device(path, 13, 64)); + assert!(!path_matches_device(path, 29, 0)); } #[test] diff --git a/local/recipes/system/redbear-sessiond/source/src/main.rs b/local/recipes/system/redbear-sessiond/source/src/main.rs index 26cbfd1c4d..3be848a0b5 100644 --- a/local/recipes/system/redbear-sessiond/source/src/main.rs +++ b/local/recipes/system/redbear-sessiond/source/src/main.rs @@ -1,4 +1,6 @@ mod acpi_watcher; +mod apply_groups; +mod caller_credentials; mod control; mod device_map; mod kernel; diff --git a/local/recipes/system/redbear-sessiond/source/src/manager.rs b/local/recipes/system/redbear-sessiond/source/src/manager.rs index 5cafade076..81f00155aa 100644 --- a/local/recipes/system/redbear-sessiond/source/src/manager.rs +++ b/local/recipes/system/redbear-sessiond/source/src/manager.rs @@ -5,12 +5,15 @@ use std::{ }; use zbus::{ + connection::Connection, fdo, interface, + message::Header, object_server::SignalEmitter, zvariant::{OwnedFd, OwnedObjectPath}, }; +use crate::caller_credentials::caller_uid; use crate::kernel; use crate::runtime_state::{InhibitorEntry, SharedRuntime}; @@ -41,6 +44,14 @@ fn dispatch_kill_request( kernel::send_signal_direct(leader as i32, signal_number as i32) } +/// Decide whether the calling D-Bus peer is allowed to perform a privileged +/// `login1` operation on the active session. Mirrors systemd-logind: +/// root and the session owner are allowed; everyone else is denied. Broader +/// cross-user policy lives in `redbear-polkit`, which sits upstream of login1. +fn is_privileged(caller_uid: u32, runtime_uid: u32) -> bool { + caller_uid == 0 || caller_uid == runtime_uid +} + #[derive(Clone, Debug)] pub struct LoginManager { runtime: SharedRuntime, @@ -128,20 +139,37 @@ impl LoginManager { Err(fdo::Error::Failed(format!("unknown login1 user uid {uid}"))) } - fn inhibit(&self, what: &str, who: &str, why: &str, mode: &str) -> fdo::Result { + async fn inhibit( + &self, + #[zbus(connection)] connection: &Connection, + #[zbus(header)] header: Header<'_>, + what: &str, + who: &str, + why: &str, + mode: &str, + ) -> fdo::Result { if mode != "block" && mode != "delay" { return Err(fdo::Error::Failed(format!( "inhibit mode must be 'block' or 'delay', got '{mode}'" ))); } + let runtime_uid = self.runtime_read().map(|r| r.uid).unwrap_or(0); + let caller_uid_value = caller_uid(connection, header).await.map_err(|err| { + fdo::Error::Failed(format!("inhibit: caller-uid lookup failed: {err}")) + })?; + if !is_privileged(caller_uid_value, runtime_uid) { + return Err(fdo::Error::AccessDenied(format!( + "inhibit: caller uid {caller_uid_value} is not authorized (runtime uid {runtime_uid})" + ))); + } + let (end_caller, end_daemon) = UnixStream::pair() .map_err(|err| fdo::Error::Failed(format!("failed to create inhibit pipe: {err}")))?; let fd_caller: StdOwnedFd = end_caller.into(); let fd_daemon: StdOwnedFd = end_daemon.into(); - let uid = self.runtime_read().map(|r| r.uid).unwrap_or(0); let pid = std::process::id(); let entry = InhibitorEntry { @@ -150,7 +178,7 @@ impl LoginManager { why: why.to_owned(), mode: mode.to_owned(), pid, - uid, + uid: caller_uid_value, }; if let Ok(mut runtime) = self.runtime.write() { @@ -162,7 +190,7 @@ impl LoginManager { } eprintln!( - "redbear-sessiond: Inhibit(what={what}, who={who}, mode={mode}) granted" + "redbear-sessiond: Inhibit(what={what}, who={who}, mode={mode}, caller_uid={caller_uid_value}) granted" ); Ok(OwnedFd::from(fd_caller)) @@ -196,8 +224,23 @@ impl LoginManager { Ok(String::from("na")) } - fn power_off(&self, _interactive: bool) -> fdo::Result<()> { - eprintln!("redbear-sessiond: PowerOff requested"); + async fn power_off( + &self, + #[zbus(connection)] connection: &Connection, + #[zbus(header)] header: Header<'_>, + _interactive: bool, + ) -> fdo::Result<()> { + let runtime_uid = self.runtime_read().map(|r| r.uid).unwrap_or(0); + let caller = caller_uid(connection, header).await.map_err(|err| { + fdo::Error::Failed(format!("power_off: caller-uid lookup failed: {err}")) + })?; + if !is_privileged(caller, runtime_uid) { + return Err(fdo::Error::AccessDenied(format!( + "power_off: caller uid {caller} is not authorized (runtime uid {runtime_uid})" + ))); + } + + eprintln!("redbear-sessiond: PowerOff requested (caller_uid={caller})"); if let Ok(mut runtime) = self.runtime.write() { runtime.preparing_for_shutdown = true; } @@ -209,8 +252,23 @@ impl LoginManager { }) } - fn reboot(&self, _interactive: bool) -> fdo::Result<()> { - eprintln!("redbear-sessiond: Reboot requested"); + async fn reboot( + &self, + #[zbus(connection)] connection: &Connection, + #[zbus(header)] header: Header<'_>, + _interactive: bool, + ) -> fdo::Result<()> { + let runtime_uid = self.runtime_read().map(|r| r.uid).unwrap_or(0); + let caller = caller_uid(connection, header).await.map_err(|err| { + fdo::Error::Failed(format!("reboot: caller-uid lookup failed: {err}")) + })?; + if !is_privileged(caller, runtime_uid) { + return Err(fdo::Error::AccessDenied(format!( + "reboot: caller uid {caller} is not authorized (runtime uid {runtime_uid})" + ))); + } + + eprintln!("redbear-sessiond: Reboot requested (caller_uid={caller})"); if let Ok(mut runtime) = self.runtime.write() { runtime.preparing_for_shutdown = true; } @@ -222,8 +280,22 @@ impl LoginManager { }) } - fn suspend(&self, _interactive: bool) -> fdo::Result<()> { - eprintln!("redbear-sessiond: Suspend requested"); + async fn suspend( + &self, + #[zbus(connection)] connection: &Connection, + #[zbus(header)] header: Header<'_>, + _interactive: bool, + ) -> fdo::Result<()> { + let runtime_uid = self.runtime_read().map(|r| r.uid).unwrap_or(0); + let caller = caller_uid(connection, header).await.map_err(|err| { + fdo::Error::Failed(format!("suspend: caller-uid lookup failed: {err}")) + })?; + if !is_privileged(caller, runtime_uid) { + return Err(fdo::Error::AccessDenied(format!( + "suspend: caller uid {caller} is not authorized (runtime uid {runtime_uid})" + ))); + } + eprintln!("redbear-sessiond: Suspend requested (caller_uid={caller})"); Ok(()) } @@ -336,7 +408,22 @@ impl LoginManager { Ok(()) } - fn terminate_session(&self, session_id: &str) -> fdo::Result<()> { + async fn terminate_session( + &self, + #[zbus(connection)] connection: &Connection, + #[zbus(header)] header: Header<'_>, + session_id: &str, + ) -> fdo::Result<()> { + let runtime_uid = self.runtime_read().map(|r| r.uid).unwrap_or(0); + let caller = caller_uid(connection, header).await.map_err(|err| { + fdo::Error::Failed(format!("terminate_session: caller-uid lookup failed: {err}")) + })?; + if !is_privileged(caller, runtime_uid) { + return Err(fdo::Error::AccessDenied(format!( + "terminate_session: caller uid {caller} is not authorized (runtime uid {runtime_uid})" + ))); + } + let mut runtime = self.runtime_write()?; if !Self::session_matches(&runtime, session_id) { return Err(fdo::Error::Failed(format!("unknown login1 session '{session_id}'"))); @@ -344,11 +431,26 @@ impl LoginManager { runtime.state = String::from("closing"); runtime.active = false; - eprintln!("redbear-sessiond: TerminateSession({session_id})"); + eprintln!("redbear-sessiond: TerminateSession({session_id}, caller_uid={caller})"); Ok(()) } - fn terminate_user(&self, uid: u32) -> fdo::Result<()> { + async fn terminate_user( + &self, + #[zbus(connection)] connection: &Connection, + #[zbus(header)] header: Header<'_>, + uid: u32, + ) -> fdo::Result<()> { + let runtime_uid = self.runtime_read().map(|r| r.uid).unwrap_or(0); + let caller = caller_uid(connection, header).await.map_err(|err| { + fdo::Error::Failed(format!("terminate_user: caller-uid lookup failed: {err}")) + })?; + if !is_privileged(caller, runtime_uid) { + return Err(fdo::Error::AccessDenied(format!( + "terminate_user: caller uid {caller} is not authorized (runtime uid {runtime_uid})" + ))); + } + let mut runtime = self.runtime_write()?; if !Self::user_matches(&runtime, uid) { return Err(fdo::Error::Failed(format!("unknown login1 user uid {uid}"))); @@ -356,11 +458,18 @@ impl LoginManager { runtime.state = String::from("closing"); runtime.active = false; - eprintln!("redbear-sessiond: TerminateUser({uid})"); + eprintln!("redbear-sessiond: TerminateUser({uid}, caller_uid={caller})"); Ok(()) } - fn kill_session(&self, session_id: &str, who: &str, signal_number: i32) -> fdo::Result<()> { + async fn kill_session( + &self, + #[zbus(connection)] connection: &Connection, + #[zbus(header)] header: Header<'_>, + session_id: &str, + who: &str, + signal_number: i32, + ) -> fdo::Result<()> { let (leader, runtime_uid) = { let runtime = self.runtime_read()?; if !Self::session_matches(&runtime, session_id) { @@ -369,9 +478,18 @@ impl LoginManager { (runtime.leader, runtime.uid) }; + let caller = caller_uid(connection, header).await.map_err(|err| { + fdo::Error::Failed(format!("kill_session: caller-uid lookup failed: {err}")) + })?; + if !is_privileged(caller, runtime_uid) { + return Err(fdo::Error::AccessDenied(format!( + "kill_session: caller uid {caller} is not authorized (runtime uid {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:?}" + "redbear-sessiond: KillSession({session_id}, who={who}, signal={signal_number}, leader={leader}, caller_uid={caller}, uid={runtime_uid}) -> {outcome:?}" ); if matches!(outcome, kernel::TerminationOutcome::Failed(_)) { @@ -386,18 +504,33 @@ impl LoginManager { Ok(()) } - fn kill_user(&self, uid: u32, signal_number: i32) -> fdo::Result<()> { - let (leader, session_id) = { + async fn kill_user( + &self, + #[zbus(connection)] connection: &Connection, + #[zbus(header)] header: Header<'_>, + uid: u32, + signal_number: i32, + ) -> fdo::Result<()> { + let (leader, session_id, runtime_uid) = { 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()) + (runtime.leader, runtime.session_id.clone(), runtime.uid) }; + let caller = caller_uid(connection, header).await.map_err(|err| { + fdo::Error::Failed(format!("kill_user: caller-uid lookup failed: {err}")) + })?; + if !is_privileged(caller, runtime_uid) { + return Err(fdo::Error::AccessDenied(format!( + "kill_user: caller uid {caller} is not authorized (runtime uid {runtime_uid})" + ))); + } + let outcome = dispatch_kill_request("user", signal_number, leader); eprintln!( - "redbear-sessiond: KillUser({uid}, signal={signal_number}, session={session_id}, leader={leader}) -> {outcome:?}" + "redbear-sessiond: KillUser({uid}, signal={signal_number}, session={session_id}, leader={leader}, caller_uid={caller}) -> {outcome:?}" ); if matches!(outcome, kernel::TerminationOutcome::Failed(_)) { @@ -604,34 +737,11 @@ mod tests { } #[test] - fn inhibit_rejects_invalid_mode() { - let manager = test_manager(); - let err = manager.inhibit("sleep", "test", "reason", "invalid").unwrap_err(); - match err { - fdo::Error::Failed(msg) => assert!(msg.contains("block") || msg.contains("delay")), - other => panic!("expected Failed error, got {other:?}"), - } - } - - #[test] - fn inhibit_tracks_entry_in_runtime() { - let runtime = shared_runtime(); - let manager = LoginManager::new( - OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/session/c1")).unwrap(), - OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(), - OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(), - runtime.clone(), - ); - - let _fd = manager - .inhibit("sleep", "testapp", "testing", "block") - .expect("inhibit should succeed"); - - let runtime_guard = runtime.read().expect("lock"); - assert_eq!(runtime_guard.inhibitors.len(), 1); - assert_eq!(runtime_guard.inhibitors[0].what, "sleep"); - assert_eq!(runtime_guard.inhibitors[0].who, "testapp"); - assert_eq!(runtime_guard.inhibitors[0].mode, "block"); + fn is_privileged_decision() { + assert!(is_privileged(0, 1000)); + assert!(is_privileged(1000, 1000)); + assert!(!is_privileged(1001, 1000)); + assert!(is_privileged(0, 0)); } #[test] diff --git a/recipes/wip/services/pipewire b/recipes/wip/services/pipewire new file mode 120000 index 0000000000..e328866871 --- /dev/null +++ b/recipes/wip/services/pipewire @@ -0,0 +1 @@ +../../../local/recipes/libs/pipewire \ No newline at end of file diff --git a/recipes/wip/services/pipewire/recipe.toml b/recipes/wip/services/pipewire/recipe.toml deleted file mode 100644 index ea85fd5340..0000000000 --- a/recipes/wip/services/pipewire/recipe.toml +++ /dev/null @@ -1,20 +0,0 @@ -#TODO not compiled or tested -# build instructions: https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/INSTALL.md -[source] -git = "https://gitlab.freedesktop.org/pipewire/pipewire" -branch = "1.4" -shallow_clone = true -[build] -template = "meson" -mesonflags = [ - "-Dtests=disabled", - "-Dpipewire-jack=disabled", - "-Dpipewire-v4l2=disabled", - "-Dspa-plugins=disabled", - "-Ddbus=disabled", - "-Dflatpak=disabled", -] -dependencies = [ - "libpulse", - "sdl2", -] diff --git a/recipes/wip/services/wireplumber b/recipes/wip/services/wireplumber new file mode 120000 index 0000000000..fedd96bb58 --- /dev/null +++ b/recipes/wip/services/wireplumber @@ -0,0 +1 @@ +../../../local/recipes/libs/wireplumber \ No newline at end of file diff --git a/recipes/wip/services/wireplumber/recipe.toml b/recipes/wip/services/wireplumber/recipe.toml deleted file mode 100644 index 80a9c0a4c6..0000000000 --- a/recipes/wip/services/wireplumber/recipe.toml +++ /dev/null @@ -1,12 +0,0 @@ -#TODO not compiled or tested -#TODO discover minimum dependencies from cmake log -[source] -git = "https://gitlab.freedesktop.org/pipewire/wireplumber" -rev = "0.5.13" -shallow_clone = true -[build] -template = "meson" -mesonflags = [ - "-Dtests=false", - "-Ddbus-tests=false", -]