0.26.0-1 setcap'd `cap_sys_nice=ep` on /usr/bin/punktfunk-host so the encoder could open an
elevated global-priority Vulkan queue. On every KDE box that ended desktop streaming outright:
KWin virtual output failed: KWin does not expose zkde_screencast_unstable_v1 to this client
reported from CachyOS on NVIDIA and on AMD, surviving a clean reinstall of host and client, and
worked around only by KWIN_WAYLAND_NO_PERMISSION_CHECKS=1.
The two cannot coexist. KWin hands out its restricted protocols — zkde_screencast_unstable_v1,
which mints our virtual output, and org_kde_kwin_fake_input, which injects input — only to a client
it can IDENTIFY, by resolving that client's /proc/<pid>/exe and matching it against an installed
.desktop's Exec=. The kernel refuses that readlink to any reader whose effective set is not a
superset of the target's PERMITTED set (cap_ptrace_access_check), and KWin holds no capabilities.
So the instant the binary carries one, KWin's executablePath() is empty, nothing matches, and the
global is never advertised — presenting exactly as a missing or mis-installed .desktop file.
Measured on CachyOS (kernel 7.1.6), same-uid reader, cap_sys_nice=ep on the target:
no capability .............................. readlink /proc/<pid>/exe OK
capability ................................. EPERM
capability + prctl(PR_SET_DUMPABLE, 1) ..... EPERM <- dumpable is NOT the gate
capability dropped + PR_SET_DUMPABLE(1) .... OK <- only an uncapped process works
The third row also rules out the reflex fix of moving the grant to systemd AmbientCapabilities=,
which lands CAP_SYS_NICE in the very same permitted set. Nothing short of not holding the
capability restores identification, so the host does not get one.
The cost is pacing only. pf-zerocopy's device create already walks REALTIME -> HIGH -> default when
a priority class is refused, and pf-frame's thread nice is a documented best-effort no-op without
the capability — so this is 0.25.0's behaviour exactly, which is the behaviour that worked.
* packaging/arch/punktfunk-host.install: grant -> revoke. post_upgrade strips the capability from
boxes that already ran 0.26.0-1's scriptlet. A pacman upgrade writes a new inode and file
capabilities do not survive that, so this is belt-and-braces for reinstall/downgrade paths.
* pf-vdisplay kwin.rs: all three "KWin does not expose zkde_screencast" errors now read
/proc/self/status and, if this process holds ANY capability, name it with its CapPrm mask and
the `setcap -r` that repairs it. The failure stays impossible to diagnose from the Wayland side
otherwise, and it is not unique to our own packaging — a hand-rolled setcap does it too.
Verified on 192.168.1.21 (CachyOS): the capability/dumpable matrix above; cargo check and
cargo clippy --all-targets -- -D warnings clean for pf-vdisplay; both new unit tests pass; and the
hint itself exercised end-to-end, silent uncapped and firing with CapPrm=0x0000000000800000 under
cap_sys_nice=ep. The shipped punktfunk-host-0.26.0-1-x86_64.pkg.tar.zst was unpacked to confirm its
.INSTALL carries the setcap on both post_install and post_upgrade.
Ships as 0.26.0-2 — packaging plus one crate, no version bump.
164 lines
8.9 KiB
Plaintext
164 lines
8.9 KiB
Plaintext
# pacman install scriptlet — mirrors the RPM %post / deb postinst.
|
|
_ensure_update_group() {
|
|
# The (empty) opt-in group for web-console-triggered updates — nobody is auto-added.
|
|
getent group punktfunk-update >/dev/null 2>&1 || groupadd --system punktfunk-update 2>/dev/null || true
|
|
}
|
|
|
|
_ensure_punktfunk_group() {
|
|
# Owns the usbip vhci attach/detach nodes (60-punktfunk.rules). Separate from 'input' on
|
|
# purpose: writing 'attach' materialises an arbitrary emulated USB device, which is a root-only
|
|
# kernel primitive and must not ride on the group users are told to join for gamepads
|
|
# (security-review 2026-08-05 M-4).
|
|
getent group punktfunk >/dev/null 2>&1 || groupadd --system punktfunk 2>/dev/null || true
|
|
}
|
|
|
|
# NO capability on the host binary — and an active removal of the one 0.26.0-1 granted.
|
|
#
|
|
# 0.26.0-1 ran `setcap cap_sys_nice=ep` here, to let the encoder open an elevated global-priority
|
|
# Vulkan queue (PyroWave shares the GPU's shader cores with the game; measured 2026-08-08 on an
|
|
# RTX 5070 Ti, the encode dispatch goes ~2 ms -> 15-18 ms at 95 % game load without it). That grant
|
|
# BROKE DESKTOP STREAMING ON EVERY KDE BOX, and it cannot be made to work — the two are mutually
|
|
# exclusive at the kernel level:
|
|
#
|
|
# KWin hands out its restricted Wayland protocols (zkde_screencast_unstable_v1, which mints our
|
|
# virtual output, and org_kde_kwin_fake_input, which injects input) only to a client it can
|
|
# IDENTIFY, by resolving that client's /proc/<pid>/exe and matching it against an installed
|
|
# .desktop's Exec= (ours is io.unom.Punktfunk.Host.desktop). The kernel refuses that readlink to
|
|
# any reader whose effective set is not a superset of the target's PERMITTED set
|
|
# (cap_ptrace_access_check), and KWin holds no capabilities. So the moment this binary carries a
|
|
# capability it becomes unidentifiable: KWin's executablePath() is empty, nothing matches, the
|
|
# globals are never advertised, and every session dies with
|
|
# "KWin does not expose zkde_screencast_unstable_v1 to this client" after 8 retries — while
|
|
# looking exactly like a missing or wrong .desktop file.
|
|
#
|
|
# Verified on CachyOS (kernel 7.1.6), same-uid reader, cap_sys_nice=ep on the target:
|
|
# no capability .............................. readlink /proc/<pid>/exe OK
|
|
# capability ................................. EPERM
|
|
# capability + prctl(PR_SET_DUMPABLE, 1) ..... EPERM <- dumpable is NOT the gate
|
|
# capability dropped + PR_SET_DUMPABLE(1) .... OK <- only a capability-free process works
|
|
#
|
|
# The third row also rules out the obvious "move it to the systemd unit": AmbientCapabilities= puts
|
|
# CAP_SYS_NICE in exactly the same permitted set and fails identically. Nothing short of not having
|
|
# the capability restores identification, so the host does not get one. The encoder already walks
|
|
# REALTIME -> HIGH -> default when the class is refused (pf-zerocopy vulkan.rs), so this costs
|
|
# pacing under a GPU-bound game and nothing else — 0.25.0's behaviour exactly.
|
|
#
|
|
# The removal below heals boxes that ran 0.26.0-1's scriptlet. A pacman upgrade writes a new inode
|
|
# and file capabilities do not survive that, so this is belt-and-braces for reinstall/downgrade
|
|
# paths — cheap, and the failure it prevents is an 8-retry session death with a misleading message.
|
|
_revoke_sched_capability() {
|
|
setcap -r usr/bin/punktfunk-host 2>/dev/null || true
|
|
}
|
|
|
|
post_install() {
|
|
_ensure_update_group
|
|
_ensure_punktfunk_group
|
|
_revoke_sched_capability
|
|
udevadm control --reload-rules 2>/dev/null || true
|
|
udevadm trigger --subsystem-match=misc 2>/dev/null || true
|
|
# Apply the UDP socket-buffer tuning now (also auto-applied at boot by systemd-sysctl).
|
|
sysctl -p /usr/lib/sysctl.d/99-punktfunk-net.conf >/dev/null 2>&1 || true
|
|
cat <<'MSG'
|
|
punktfunk-host installed.
|
|
1. Add yourself to the 'input' group for virtual gamepads:
|
|
sudo usermod -aG input "$USER" # then re-login
|
|
Only if you want the virtual Steam Deck pad (usbip), ALSO join 'punktfunk':
|
|
sudo usermod -aG punktfunk "$USER"
|
|
That group can emulate arbitrary USB devices — join it only on a machine you trust.
|
|
2. Pick a backend config (gamescope is the no-desktop default on SteamOS/Deck):
|
|
mkdir -p ~/.config/punktfunk
|
|
cp /usr/share/punktfunk/host.env.bazzite ~/.config/punktfunk/host.env
|
|
3. Enable the host:
|
|
systemctl --user enable --now punktfunk-host
|
|
|
|
NOTE: encode is NVENC-only. Install 'nvidia-utils' on an NVIDIA host. An AMD Steam Deck is NOT
|
|
yet supported — it needs a VAAPI (hevc_vaapi) encoder backend (see packaging/arch/README.md).
|
|
MSG
|
|
# Firewall: stock Arch ships none (ports already open); CachyOS ships ufw; some spins (EndeavourOS)
|
|
# enable firewalld. We install a ufw app profile AND firewalld service definitions but never touch
|
|
# the running firewall — just point the way for whichever is active.
|
|
if command -v ufw >/dev/null 2>&1; then
|
|
cat <<'MSG'
|
|
|
|
4. ufw is installed — open the streaming ports once (native-only host shown; add
|
|
'punktfunk-gamestream' as well for Moonlight compat):
|
|
sudo ufw allow punktfunk-native
|
|
MSG
|
|
fi
|
|
if command -v firewall-cmd >/dev/null 2>&1; then
|
|
cat <<'MSG'
|
|
|
|
4. firewalld is active — open the streaming ports once (native-only host shown; add
|
|
'punktfunk-gamestream' as well for Moonlight compat):
|
|
sudo firewall-cmd --reload # load the new service def
|
|
sudo firewall-cmd --permanent --add-service=punktfunk-native
|
|
sudo firewall-cmd --reload
|
|
MSG
|
|
fi
|
|
# Conflicting Moonlight-compatible host (Sunshine/Apollo/...): reuse the host's own detector so
|
|
# the warning lives in one place. Exit 1 = found; never fail the install on it.
|
|
if command -v punktfunk-host >/dev/null 2>&1; then
|
|
if ! conflict="$(punktfunk-host detect-conflicts 2>/dev/null)"; then
|
|
printf '\n%s\n' "$conflict"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
post_upgrade() {
|
|
_ensure_update_group
|
|
# Also on UPGRADE, not just post_install: 'punktfunk' was introduced in 0.25.0, so every box that
|
|
# reached it by `pacman -Syu` from 0.24.x ran only this function and never got the group at all —
|
|
# leaving 60-punktfunk.rules to chgrp to a nonexistent group, the vhci attach/detach nodes
|
|
# root-only, and the virtual Steam Deck pad silently unable to attach. groupadd is idempotent, so
|
|
# this is a no-op on boxes that installed fresh.
|
|
_ensure_punktfunk_group
|
|
# Strip the cap_sys_nice 0.26.0-1 granted: it makes the host unidentifiable to KWin (see above).
|
|
_revoke_sched_capability
|
|
udevadm control --reload-rules 2>/dev/null || true
|
|
sysctl -p /usr/lib/sysctl.d/99-punktfunk-net.conf >/dev/null 2>&1 || true
|
|
_warn_stale_firewall_ports
|
|
}
|
|
|
|
# An already-open firewall does NOT pick up a port we added to a profile.
|
|
#
|
|
# ufw expands an app profile into concrete rules when you run `ufw allow`, and stores THOSE. Editing
|
|
# /etc/ufw/applications.d later — which is all a package upgrade does — changes nothing about the
|
|
# rules already installed. firewalld is friendlier (its permanent config names the service, so a
|
|
# reload re-reads the XML) but still needs that reload. Either way the operator has an old rule and
|
|
# no reason to suspect it.
|
|
#
|
|
# That is not hypothetical: 47993 (plugin UIs, a separate origin from the console) arrived exactly
|
|
# this way, and on an upgraded ufw box every plugin interface silently became an empty panel in the
|
|
# console. So on upgrade, look at what is actually open and say so — still without touching the
|
|
# running firewall, which stays the operator's call.
|
|
_warn_stale_firewall_ports() {
|
|
# `ufw status verbose` prints each rule with its EXPANDED ports — "47992/tcp (punktfunk-web)"
|
|
# before the refresh, "47992,47993/tcp (punktfunk-web)" after — so one listing answers both "is
|
|
# the profile allowed at all" and "does that rule know the new port". (Plain `ufw status` prints
|
|
# the profile NAME instead, which cannot tell the two apart.)
|
|
if command -v ufw >/dev/null 2>&1 &&
|
|
ufw status verbose 2>/dev/null | grep -q 'punktfunk-web' &&
|
|
! ufw status verbose 2>/dev/null | grep -q '47993'; then
|
|
cat <<'MSG'
|
|
|
|
punktfunk: your ufw rule for 'punktfunk-web' predates TCP 47993, the separate origin plugin UIs
|
|
are served from. Until it is refreshed, plugin interfaces will not load in the web console:
|
|
sudo ufw app update punktfunk-web && sudo ufw reload
|
|
MSG
|
|
fi
|
|
# `--info-service` asks the DAEMON, which answers from the definition it loaded at its last
|
|
# (re)start — precisely the stale copy we are warning about. The file on disk already says 47993.
|
|
if command -v firewall-cmd >/dev/null 2>&1 &&
|
|
firewall-cmd --state >/dev/null 2>&1 &&
|
|
firewall-cmd --query-service=punktfunk-web >/dev/null 2>&1 &&
|
|
! firewall-cmd --info-service=punktfunk-web 2>/dev/null | grep -q '47993'; then
|
|
cat <<'MSG'
|
|
|
|
punktfunk: the punktfunk-web firewalld service now also covers TCP 47993, the separate origin
|
|
plugin UIs are served from. Reload so the running firewall picks it up, or plugin interfaces will
|
|
not load in the web console:
|
|
sudo firewall-cmd --reload
|
|
MSG
|
|
fi
|
|
}
|