Our virtual DualSense wakes Bazzite's ds_inhibit into an SELinux audit storm that freezes the stream #248

Merged
enricobuehler merged 1 commits from worktree-ds-inhibit-storm into main 2026-08-15 13:08:06 +00:00
5 changed files with 195 additions and 0 deletions
+110
View File
@@ -140,6 +140,78 @@ fn degrade_if_no_uhid(chosen: GamepadPref) -> GamepadPref {
chosen
}
/// Detection half of [`warn_if_ds_inhibit_storm`], split out for tests: `true` when a process
/// named `steamos-manager` is running (its full name fits `comm`'s 15-char limit exactly) AND
/// SELinux is enforcing (the `enforce` file reads `1`). Both halves are required for the storm:
/// a permissive box logs one denial per walk and moves on, and without steamos-manager there is
/// no ds_inhibit to trigger.
#[cfg(target_os = "linux")]
fn ds_inhibit_storm_risk(proc_root: &std::path::Path, enforce: &std::path::Path) -> bool {
let enforcing = std::fs::read_to_string(enforce).is_ok_and(|v| v.trim() == "1");
if !enforcing {
return false;
}
std::fs::read_dir(proc_root)
.ok()
.into_iter()
.flatten()
.flatten()
.any(|e| {
std::fs::read_to_string(e.path().join("comm"))
.is_ok_and(|c| c.trim() == "steamos-manager")
})
}
/// One-shot diagnostic for the Bazzite/SteamOS ds_inhibit audit storm: Valve's `steamos-manager`
/// reacts to every open/close of a `hid-playstation` hidraw — exactly what our virtual
/// DualSense / DualShock 4 is; it has no VID/PID or virtual/uhid filtering — by walking
/// `/proc/*/fd/` to see whether Steam holds the node. Under SELinux enforcing that walk is
/// denied (`steamos_manager_t` lacks `sys_ptrace`/`dac_*`; measured ~324 AVCs/sec on Bazzite),
/// and `setroubleshootd` amplifies the flood into a box-wide fork storm that starves the stream
/// (gamescope 0 fps, encode submit ~150 ms/frame). The audit lines read `comm="tokio-rt-worker"`
/// and look like us — they are steamos-manager's (check `scontext=`).
///
/// Warn-only, never degrade: a per-pad fold has no wire channel back to the client
/// (`Welcome::gamepad` only describes the session default), and it would strip the DS5 feature
/// set on exactly the platform where users want it. The real fix is the shipped SELinux drop-in;
/// this warning cannot see the policy store (root-only), so it fires even where that drop-in is
/// already installed — it names that, and puts the cause in OUR logs so the audit-log trap above
/// doesn't get someone blaming the encoder again.
#[cfg(target_os = "linux")]
fn warn_if_ds_inhibit_storm(chosen: GamepadPref) {
use std::sync::atomic::{AtomicBool, Ordering};
static ONCE: AtomicBool = AtomicBool::new(true);
// Selection criterion is the bound driver (`sony`/`playstation`) plus the touchpad's mouse
// node — i.e. the hid-playstation backends. (The kernel registers the touchpad from its own
// hardcoded DS5/DS4 handling, so no descriptor shaping can duck the selection.)
let playstation = matches!(
chosen,
GamepadPref::DualSense | GamepadPref::DualSenseEdge | GamepadPref::DualShock4
);
if !playstation
|| !ds_inhibit_storm_risk(
std::path::Path::new("/proc"),
std::path::Path::new("/sys/fs/selinux/enforce"),
)
|| !ONCE.swap(false, Ordering::Relaxed)
{
return;
}
tracing::warn!(
gamepad = chosen.as_str(),
"steamos-manager is running and SELinux is enforcing — its ds_inhibit scans /proc on \
every open/close of this pad's hidraw, the scan is denied at hundreds of AVCs/sec, and \
setroubleshootd can amplify that into a box-wide stall that starves the stream. Install \
the shipped SELinux drop-in (`sudo punktfunk-sysext reapply`, or `sudo semodule -i \
/usr/share/punktfunk/selinux/punktfunk-ds-inhibit.cil`) — harmless if already installed. \
Masking setroubleshootd (`sudo systemctl mask --now setroubleshootd`) hardens the box \
against any audit flood. Details: packaging/bazzite/README.md."
);
}
#[cfg(not(target_os = "linux"))]
fn warn_if_ds_inhibit_storm(_chosen: GamepadPref) {}
/// The Valve product id (`28DE:xxxx`) a virtual Steam backend enumerates as, or `None` for a
/// non-Steam backend. This is the identity the conflict gate compares against the *physical* Valve
/// devices attached to the host: only a genuine duplicate (same VID **and** PID) confuses Steam
@@ -323,6 +395,10 @@ pub(super) fn resolve_gamepad(pref: GamepadPref) -> GamepadPref {
// The XUSB escape hatch can only present a 360 identity, so the One S / Elite wishes fold when
// `PUNKTFUNK_XBOX_BACKEND=xusb` is set.
let chosen = degrade_xbox_identity(chosen);
// Bazzite/SteamOS heads-up, warn-only (see the fn for why never a degrade): a
// hid-playstation pad on a box running steamos-manager under SELinux enforcing risks the
// ds_inhibit audit storm.
warn_if_ds_inhibit_storm(chosen);
match pref {
GamepadPref::Auto => {
// The operator's env knob deserves a diagnostic when it didn't drive the
@@ -519,4 +595,38 @@ mod tests {
assert_eq!(steam_backend_product(Xbox360), None);
assert_eq!(steam_backend_product(SwitchPro), None);
}
// The ds_inhibit-storm detection needs BOTH halves: steamos-manager running AND SELinux
// enforcing. A permissive box logs one denial per walk without storming, and without
// steamos-manager there is no ds_inhibit — either alone must stay silent.
#[cfg(target_os = "linux")]
#[test]
fn ds_inhibit_storm_risk_needs_both_halves() {
use super::ds_inhibit_storm_risk;
let dir = tempfile::tempdir().unwrap();
let proc_root = dir.path().join("proc");
let enforce = dir.path().join("enforce");
std::fs::create_dir_all(proc_root.join("123")).unwrap();
// Enforcing + steamos-manager present → risk.
std::fs::write(proc_root.join("123/comm"), "steamos-manager\n").unwrap();
std::fs::write(&enforce, "1\n").unwrap();
assert!(ds_inhibit_storm_risk(&proc_root, &enforce));
// Permissive (or SELinux absent — the enforce file unreadable) → no risk.
std::fs::write(&enforce, "0\n").unwrap();
assert!(!ds_inhibit_storm_risk(&proc_root, &enforce));
assert!(!ds_inhibit_storm_risk(
&proc_root,
&dir.path().join("missing")
));
// Enforcing but no steamos-manager (a comm that merely CONTAINS the name must not
// match — the scan compares the whole trimmed comm) → no risk.
std::fs::write(&enforce, "1\n").unwrap();
std::fs::write(proc_root.join("123/comm"), "not-steamos\n").unwrap();
assert!(!ds_inhibit_storm_risk(&proc_root, &enforce));
std::fs::write(proc_root.join("123/comm"), "steamos-managerX\n").unwrap();
assert!(!ds_inhibit_storm_risk(&proc_root, &enforce));
}
}
+23
View File
@@ -504,6 +504,29 @@ desktop viewer.
`ExecStart=/usr/bin/punktfunk-host serve --gamestream` (or bare `serve` for native-only) if needed
(section 5).
- **Stream lags, then freezes, with a DualSense-type client pad (SELinux enforcing).** The virtual
DualSense / DualShock 4 binds the kernel's `hid-playstation` driver, and Valve's `ds_inhibit`
(inside `steamos-manager`, shipped on Bazzite) reacts to *any* such hidraw by walking
`/proc/*/fd/` on every open/close. SELinux denies `steamos_manager_t` that walk, spraying
**~324 `avc: denied` per second**, and `setroubleshootd` amplifies the flood into a box-wide
fork storm that starves the stream (gamescope 0 fps, `tx_mbps` collapsing) — measured live on
Bazzite 43, 2026-08-15. Two traps while diagnosing: the AVC lines read `comm="tokio-rt-worker"`
— that is **steamos-manager, not punktfunk** (check `scontext=…steamos_manager_t…`); and once
started the setroubleshootd storm **outlives the denials by 15+ minutes**, so the box stays
starved after the pad is gone. Fixes:
- punktfunk ships a `dontaudit` SELinux drop-in that silences the flood (ds_inhibit then simply
leaves the pad uninhibited — harmless). The sysext installs it automatically on
install/update; on an existing install run `sudo punktfunk-sysext reapply`. On a layered or
bootc host: `sudo semodule -i /usr/share/punktfunk/selinux/punktfunk-ds-inhibit.cil`
(remove with `sudo semodule -r punktfunk-ds-inhibit`).
- **Hardening (recommended on any streaming host):** `sudo systemctl mask --now
setroubleshootd`. It is purely a desktop alert daemon — nothing depends on it
(`systemctl list-dependencies --reverse setroubleshootd` returns only itself) — and masking
it makes the box robust against *any* AVC burst, not just this one. Reversible with `unmask`.
- Workaround with the feature loss: set the **client's** Controller type to Xbox 360 (uinput,
no `hid-playstation`) — costs adaptive triggers, lightbar and touchpad. The host-side
`PUNKTFUNK_GAMEPAD` knob does **not** help: an explicit client choice outranks it.
- **Moonlight can't see the host.** Ensure UDP 5353 (mDNS) and the GameStream ports are open
(section 6) and client + host are on the same L2 LAN segment.
@@ -0,0 +1,28 @@
; SELinux drop-in for Bazzite / SteamOS-derived hosts: silence the audit flood Valve's
; ds_inhibit (inside steamos-manager) produces around ANY `hid-playstation` hidraw —
; which is exactly what punktfunk's virtual DualSense / DualShock 4 is (ds_inhibit
; selects by bound driver + touchpad mouse node; it has no VID/PID or virtual filtering).
;
; ds_inhibit reacts to every open/close of such a hidraw by walking /proc/*/fd/ to see
; whether Steam holds the node. The distro policy denies steamos_manager_t the three
; capabilities that walk needs, so the scan never succeeds, the state machine never
; latches, and every open/close sprays denials — ~324 AVCs/sec measured on Bazzite 43
; (2026-08-15). setroubleshootd then amplifies the flood into a box-wide fork storm
; (267+ procs/sec) that starves the stream: gamescope 0 fps, encode submit ~150 ms/frame.
;
; dontaudit, NOT allow: granting another vendor's daemon sys_ptrace/dac_override is not
; ours to do — silencing the audit noise is. The scan keeps failing quietly and
; ds_inhibit simply leaves the pad uninhibited, which is what we want anyway (the
; "inhibit" would disable the touchpad-as-mouse of a pad we created for a game). The
; underlying gap — steamos_manager_t lacking what its own ds_inhibit needs — is a
; Bazzite/Valve policy bug; this drop-in is the containment we can ship. A bare,
; un-amplified AVC is cheap, but with dontaudit not even that remains.
;
; Installed idempotently by `punktfunk-sysext` post_merge (and best-effort by the RPM
; %post) wherever steamos-manager exists; the policy store is host state, so a sysext
; image cannot carry the module itself — only this source. By hand:
; sudo semodule -i punktfunk-ds-inhibit.cil # remove: sudo semodule -r punktfunk-ds-inhibit
; ⚠ The installers key idempotence on the module NAME (= this filename): if these rules
; ever change, rename the file (and every reference) so existing installs converge.
(dontaudit steamos_manager_t self (capability (dac_override dac_read_search sys_ptrace)))
(dontaudit steamos_manager_t self (cap_userns (sys_ptrace)))
+16
View File
@@ -191,6 +191,22 @@ post_merge() {
# Re-fire the vhci rule against the (possibly already-present) controller so attach/detach pick up
# the input-group ownership even when the module's original add event predated the reloaded rule.
udevadm trigger --subsystem-match=platform --sysname-match='vhci_hcd.*' 2>/dev/null || :
# ds_inhibit dontaudit drop-in (Bazzite ships steamos-manager; keyed on its binary): Valve's
# ds_inhibit walks /proc/*/fd on every open/close of a hid-playstation hidraw — exactly what the
# virtual DualSense is — and SELinux denies steamos_manager_t that walk at ~324 AVCs/sec;
# setroubleshootd amplifies the flood into a box-wide stall that starves the stream (gamescope
# 0 fps, encode submit ~150 ms/frame). The policy STORE is host state (/var/lib/selinux), so the
# image carries only the CIL source and the module must be inserted here. Keyed on the module
# NAME for idempotence (a policy rebuild costs seconds every merge otherwise — if the rules ever
# change, RENAME the file and every reference so existing installs converge). Rationale and the
# dontaudit-vs-allow choice: the .cil header / packaging/bazzite/README.md.
if command -v semodule >/dev/null 2>&1 && [ -e /usr/lib/steamos-manager ] \
&& [ -f /usr/share/punktfunk/selinux/punktfunk-ds-inhibit.cil ] \
&& ! semodule -l 2>/dev/null | grep -qx punktfunk-ds-inhibit; then
echo "installing SELinux drop-in 'punktfunk-ds-inhibit' (silences the steamos-manager ds_inhibit audit flood)…"
semodule -i /usr/share/punktfunk/selinux/punktfunk-ds-inhibit.cil \
|| echo "!! semodule -i failed — the ds_inhibit audit flood stays live; see packaging/bazzite/README.md" >&2
fi
# The /etc payload a sysext can't carry. The gamescope-session drop-in is %config(noreplace):
# only seed it, never clobber a local edit. The tray autostart entry is not user config.
if [ -f "$ETC_SRC/gamescope-session-plus/sessions.d/steam" ] \
+18
View File
@@ -450,6 +450,13 @@ install -Dm0644 packaging/kde/host.env %{buildroot}%{_datadir}/%
# screencast/virtual-output grant ships as io.unom.Punktfunk.Host.desktop, installed above).
install -d %{buildroot}%{_datadir}/%{name}/bazzite
install -Dm0755 packaging/bazzite/kde-desktop-setup.sh %{buildroot}%{_datadir}/%{name}/bazzite/kde-desktop-setup.sh
# SELinux dontaudit drop-in for Bazzite/SteamOS: Valve's ds_inhibit (steamos-manager) walks
# /proc/*/fd on every open/close of a hid-playstation hidraw — our virtual DualSense — and the
# denied walk sprays ~324 AVCs/sec, which setroubleshootd amplifies into a box-wide stall that
# starves the stream. Shipped as CIL source (the policy STORE is host state); inserted by %%post
# below / punktfunk-sysext post_merge where steamos-manager exists. See the file's header.
install -Dm0644 packaging/bazzite/punktfunk-ds-inhibit.cil \
%{buildroot}%{_datadir}/%{name}/selinux/punktfunk-ds-inhibit.cil
# Layered-update helper for rpm-ostree hosts: `rpm-ostree upgrade` only re-resolves layered
# packages when the BASE changes, so a frozen Bazzite base pins punktfunk forever. The script
# forces a re-resolve of just this layer (--uninstall + --install of the same names in one
@@ -674,6 +681,17 @@ udevadm trigger --subsystem-match=misc 2>/dev/null || :
# Apply the UDP socket-buffer tuning (also auto-applied at boot by systemd-sysctl; on rpm-ostree
# it takes effect on the next boot into the layered deployment).
sysctl -p %{_prefix}/lib/sysctl.d/99-punktfunk-net.conf >/dev/null 2>&1 || :
# Bazzite/SteamOS only (keyed on the steamos-manager binary): insert the ds_inhibit dontaudit
# drop-in — Valve's ds_inhibit walks /proc on every open/close of our virtual DualSense's hidraw,
# the denied walk sprays AVCs, and setroubleshootd amplifies that into a box-wide stall (see
# packaging/bazzite/punktfunk-ds-inhibit.cil). Keyed on the module NAME for idempotence (a policy
# rebuild costs seconds — rename the file if the rules ever change). Best-effort and never fatal:
# rpm-ostree's scriptlet sandbox may refuse semodule; the sysext post_merge and the README's
# manual command cover that path.
if command -v semodule >/dev/null 2>&1 && [ -e /usr/lib/steamos-manager ] &&
! semodule -l 2>/dev/null | grep -qx punktfunk-ds-inhibit; then
semodule -i %{_datadir}/%{name}/selinux/punktfunk-ds-inhibit.cil >/dev/null 2>&1 || :
fi
echo "punktfunk installed. Add yourself to the 'input' group (sudo usermod -aG input \$USER)"
# Naming only the usbip pad here is how a Nobara host shipped broken: its owner had no Deck pad, so
# they correctly skipped this group — and then every managed gamescope takeover degraded silently,