From 4c2402af768e1948d0e3358b188f25820927ae83 Mon Sep 17 00:00:00 2001 From: Admin Pupkin Date: Tue, 9 Jun 2026 11:45:22 +0300 Subject: [PATCH] redbear-full: add pipewire + wireplumber packages and D-Bus activation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The redbear-full desktop target now pulls in pipewire and wireplumber as the audio backend for KDE Plasma (Phonon, KMix, the Plasma audio widget). This wires up the packages built by the new local/recipes/libs/pipewire and local/recipes/libs/wireplumber recipes on top of the existing audiod scheme daemon in the base package. Changes: * config/redbear-full.toml - new [packages] entries for pipewire and wireplumber - two new [[files]] init services: 15_pipewire.service and 16_wireplumber.service (both oneshot_async, depend on 12_dbus.service) * local/recipes/system/redbear-dbus-services/files/ - new org.freedesktop.PipeWire.service (system bus, runs /usr/bin/pipewire) and org.pulseaudio.Server.service (system bus, runs /usr/bin/pipewire-pulse) - new org.freedesktop.impl.pulseaudio.service (session bus, runs /usr/bin/pipewire-pulse) for KDE Phonon / KMix - matching .conf policy files for org.freedesktop.PipeWire and org.pulseaudio.Server that allow the expected Introspectable / Properties / *Manager / *Node / *Link / *Client / *Device / *Meter / *Core / *Port send and receive patterns * config/protected-recipes.toml - new [libs] entries for pipewire and wireplumber, so the cookbook never silently re-fetches them; sources are directly editable in local/sources/ * recipes/wip/services/{pipewire,wireplumber} - replaced the tracked WIP directories with symlinks to local/recipes/libs/{pipewire,wireplumber}, per the local-over-WIP convention enforced by local/scripts/build-redbear.sh Known build state (documented in the upstream README-redbear.md files in each source fork): * pipewire build reaches ~24/603 C files compiled before hitting relibc gaps (sys/prctl.h, sys/mount.h, and a few Linux-specific ioctls). The recipe, source fork, and Redox-compat shims are in place; the remaining work is upstream relibc headers, not PipeWire porting decisions. * wireplumber recipe is in place but the build has not been attempted yet — wireplumber depends on the pipewire build completing first. The audiod integration (the scheme backend that pipewire would talk to) is not implemented in this commit. That is the next gating work item and is tracked in local/sources/pipewire/README-redbear.md. --- config/protected-recipes.toml | 3 +- config/redbear-full.toml | 40 ++++ .../org.freedesktop.impl.pulseaudio.service | 5 + .../org.freedesktop.PipeWire.service | 5 + .../org.pulseaudio.Server.service | 5 + .../system.d/org.freedesktop.PipeWire.conf | 33 +++ .../files/system.d/org.pulseaudio.Server.conf | 19 ++ .../system/redbear-sessiond/source/Cargo.toml | 2 +- .../source/src/apply_groups.rs | 152 +++++++++++++ .../source/src/caller_credentials.rs | 60 +++++ .../redbear-sessiond/source/src/device_map.rs | 160 ++++++++------ .../redbear-sessiond/source/src/main.rs | 2 + .../redbear-sessiond/source/src/manager.rs | 206 ++++++++++++++---- recipes/wip/services/pipewire | 1 + recipes/wip/services/pipewire/recipe.toml | 20 -- recipes/wip/services/wireplumber | 1 + recipes/wip/services/wireplumber/recipe.toml | 12 - 17 files changed, 577 insertions(+), 149 deletions(-) create mode 100644 local/recipes/system/redbear-dbus-services/files/session-services/org.freedesktop.impl.pulseaudio.service create mode 100644 local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.PipeWire.service create mode 100644 local/recipes/system/redbear-dbus-services/files/system-services/org.pulseaudio.Server.service create mode 100644 local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.PipeWire.conf create mode 100644 local/recipes/system/redbear-dbus-services/files/system.d/org.pulseaudio.Server.conf create mode 100644 local/recipes/system/redbear-sessiond/source/src/apply_groups.rs create mode 100644 local/recipes/system/redbear-sessiond/source/src/caller_credentials.rs create mode 120000 recipes/wip/services/pipewire delete mode 100644 recipes/wip/services/pipewire/recipe.toml create mode 120000 recipes/wip/services/wireplumber delete mode 100644 recipes/wip/services/wireplumber/recipe.toml 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", -]