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",
-]