Our virtual DualSense wakes Bazzite's ds_inhibit into an SELinux audit storm that freezes the stream #248
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
@@ -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" ] \
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user