redbear-full: add pipewire + wireplumber packages and D-Bus activation

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.
This commit is contained in:
2026-06-09 11:45:22 +03:00
parent a68b495690
commit 4c2402af76
17 changed files with 577 additions and 149 deletions
+2 -1
View File
@@ -63,7 +63,8 @@ recipes = [
# Red Bear custom libs (no stubs)
[libs]
recipes = [
"libudev", "zbus", "libqrencode",
"zbus", "libqrencode",
"pipewire", "wireplumber",
]
# Red Bear Wayland
+40
View File
@@ -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 = """
@@ -0,0 +1,5 @@
[D-BUS Service]
Name=org.freedesktop.impl.pulseaudio
Exec=/usr/bin/pipewire-pulse
User=root
SystemdService=pipewire-pulse.service
@@ -0,0 +1,5 @@
[D-BUS Service]
Name=org.freedesktop.PipeWire
Exec=/usr/bin/pipewire
User=root
SystemdService=pipewire.service
@@ -0,0 +1,5 @@
[D-BUS Service]
Name=org.pulseaudio.Server
Exec=/usr/bin/pipewire-pulse
User=root
SystemdService=pipewire-pulse.service
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.freedesktop.PipeWire"/>
<allow send_destination="org.freedesktop.PipeWire"/>
<allow receive_sender="org.freedesktop.PipeWire"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.PipeWire"
send_interface="org.freedesktop.DBus.Introspectable"/>
<allow send_destination="org.freedesktop.PipeWire"
send_interface="org.freedesktop.DBus.Properties"/>
<allow send_destination="org.freedesktop.PipeWire"
send_interface="org.freedesktop.PipeWire.Manager"/>
<allow send_destination="org.freedesktop.PipeWire"
send_interface="org.freedesktop.PipeWire.Core"/>
<allow send_destination="org.freedesktop.PipeWire"
send_interface="org.freedesktop.PipeWire.Node"/>
<allow send_destination="org.freedesktop.PipeWire"
send_interface="org.freedesktop.PipeWire.Port"/>
<allow send_destination="org.freedesktop.PipeWire"
send_interface="org.freedesktop.PipeWire.Link"/>
<allow send_destination="org.freedesktop.PipeWire"
send_interface="org.freedesktop.PipeWire.Client"/>
<allow send_destination="org.freedesktop.PipeWire"
send_interface="org.freedesktop.PipeWire.Device"/>
<allow send_destination="org.freedesktop.PipeWire"
send_interface="org.freedesktop.PipeWire.Meter"/>
<allow receive_sender="org.freedesktop.PipeWire"/>
</policy>
</busconfig>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.pulseaudio.Server"/>
<allow send_destination="org.pulseaudio.Server"/>
<allow receive_sender="org.pulseaudio.Server"/>
</policy>
<policy context="default">
<allow send_destination="org.pulseaudio.Server"
send_interface="org.freedesktop.DBus.Introspectable"/>
<allow send_destination="org.pulseaudio.Server"
send_interface="org.freedesktop.DBus.Properties"/>
<allow send_destination="org.pulseaudio.Server"
send_interface="org.pulseaudio.Server"/>
<allow receive_sender="org.pulseaudio.Server"/>
</policy>
</busconfig>
@@ -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"
@@ -0,0 +1,152 @@
//! Supplementary-groups setter for spawned user sessions.
//!
//! The Redox kernel added a `groups: Vec<u32>` 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::<u32>());
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<libc::gid_t> = 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 `<pid>:<handle>:<name>`; we parse it to
/// recover the handle number.
#[cfg(target_os = "redox")]
#[allow(dead_code)]
fn open_groups_handle() -> io::Result<std::fs::File> {
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");
}
}
}
@@ -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<u32, ZbusError> {
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<bool, ZbusError> {
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));
}
}
@@ -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<PathBuf> {
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<u32> {
let suffix = name.strip_prefix(prefix)?;
if suffix.is_empty() {
return None;
}
suffix.parse::<u32>().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]
@@ -1,4 +1,6 @@
mod acpi_watcher;
mod apply_groups;
mod caller_credentials;
mod control;
mod device_map;
mod kernel;
@@ -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<OwnedFd> {
async fn inhibit(
&self,
#[zbus(connection)] connection: &Connection,
#[zbus(header)] header: Header<'_>,
what: &str,
who: &str,
why: &str,
mode: &str,
) -> fdo::Result<OwnedFd> {
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]
+1
View File
@@ -0,0 +1 @@
../../../local/recipes/libs/pipewire
-20
View File
@@ -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",
]
+1
View File
@@ -0,0 +1 @@
../../../local/recipes/libs/wireplumber
@@ -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",
]