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:
@@ -63,7 +63,8 @@ recipes = [
|
||||
# Red Bear custom libs (no stubs)
|
||||
[libs]
|
||||
recipes = [
|
||||
"libudev", "zbus", "libqrencode",
|
||||
"zbus", "libqrencode",
|
||||
"pipewire", "wireplumber",
|
||||
]
|
||||
|
||||
# Red Bear Wayland
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
[D-BUS Service]
|
||||
Name=org.freedesktop.impl.pulseaudio
|
||||
Exec=/usr/bin/pipewire-pulse
|
||||
User=root
|
||||
SystemdService=pipewire-pulse.service
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
[D-BUS Service]
|
||||
Name=org.freedesktop.PipeWire
|
||||
Exec=/usr/bin/pipewire
|
||||
User=root
|
||||
SystemdService=pipewire.service
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
[D-BUS Service]
|
||||
Name=org.pulseaudio.Server
|
||||
Exec=/usr/bin/pipewire-pulse
|
||||
User=root
|
||||
SystemdService=pipewire-pulse.service
|
||||
+33
@@ -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]
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../local/recipes/libs/pipewire
|
||||
@@ -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",
|
||||
]
|
||||
Symlink
+1
@@ -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",
|
||||
]
|
||||
Reference in New Issue
Block a user