fix(gamescope): ship a packaged privilege path for the DM-stop takeover
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 55s
android / android (push) Successful in 12m5s
decky / build-publish (push) Successful in 25s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 46s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
ci / bench (push) Successful in 6m29s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m5s
docker / deploy-docs (push) Successful in 27s
deb / build-publish (push) Successful in 8m40s
arch / build-publish (push) Successful in 14m51s
deb / build-publish-host (push) Successful in 9m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 15m32s
apple / swift (push) Successful in 23m28s
apple / screenshots (push) In progress
ci / rust (push) Successful in 26m17s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m37s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 55s
android / android (push) Successful in 12m5s
decky / build-publish (push) Successful in 25s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 46s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
ci / bench (push) Successful in 6m29s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m5s
docker / deploy-docs (push) Successful in 27s
deb / build-publish (push) Successful in 8m40s
arch / build-publish (push) Successful in 14m51s
deb / build-publish-host (push) Successful in 9m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 15m32s
apple / swift (push) Successful in 23m28s
apple / screenshots (push) In progress
ci / rust (push) Successful in 26m17s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m37s
Field report (Nobara, 0.19.2): entering Game Mode mid-stream left the monitor on and the stream mirroring at the desktop's resolution. Root cause: the DM-flavor takeover needs privilege to stop plasmalogin, the polkit rule is a docs-only manual step nobody installs, so every managed entry silently degraded to ATTACH — and with a physical display connected the attach path mirrors the box's own session at its own mode. Ship the privilege with the packages instead: a root helper (libexec/punktfunk/pf-dm-helper, verbs stop|restore) behind its own polkit action (io.unom.punktfunk.dm-helper, allow_any — the same mechanism Nobara's os-session-select uses, and the helper derives the DM unit from the display-manager.service symlink itself so callers never name a unit across the privilege boundary). The host tries the plain system-bus verbs first (root / operator rule still take precedence), then pkexec's the helper; the restore paths (idle restore + in-stream desktop-switch honor) use the same ladder so a takeover that needed the helper can always be undone by it. Packaged in rpm/deb/arch (Arch under /usr/lib/punktfunk with the policy's exec.path annotation rewritten; the host probes both layouts). Nix is left out deliberately: store paths can't match the probe, NixOS keeps the manual rule. Docs updated — the rule snippet stays as the scoped alternative. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1139,18 +1139,67 @@ fn dm_survives_masked_unit(dm: &str) -> bool {
|
||||
dm == "sddm.service"
|
||||
}
|
||||
|
||||
/// Stop the display manager for a takeover on a mask-fragile DM flavor. Plain `systemctl stop` on
|
||||
/// the SYSTEM bus — succeeds as root or under an operator polkit rule scoped to the DM unit (see
|
||||
/// docs); fails cleanly otherwise ("interactive authentication required"), in which case the
|
||||
/// caller degrades to attach.
|
||||
fn try_stop_display_manager(dm: &str) -> bool {
|
||||
Command::new("systemctl")
|
||||
.args(["stop", dm])
|
||||
/// The packaged privileged fallback for the display-manager takeover verbs: a root helper behind
|
||||
/// its own polkit action (`io.unom.punktfunk.dm-helper`, `allow_any` — the mechanism these
|
||||
/// distros use for their own session switcher, e.g. Nobara's `os-session-select`), so the managed
|
||||
/// takeover works out of the box on mask-fragile DM flavors with no hand-installed polkit rule.
|
||||
/// The helper derives the DM unit from the `display-manager.service` symlink itself, so this
|
||||
/// process never gets to name an arbitrary unit across the privilege boundary. Two layouts: the
|
||||
/// rpm/deb `libexec` path (what the shipped policy annotates) and Arch's `/usr/lib/<pkg>` (its
|
||||
/// PKGBUILD rewrites the annotation to match).
|
||||
const DM_HELPER_PATHS: &[&str] = &[
|
||||
"/usr/libexec/punktfunk/pf-dm-helper",
|
||||
"/usr/lib/punktfunk/pf-dm-helper",
|
||||
];
|
||||
|
||||
/// Run the packaged DM helper (`stop` | `restore`) via pkexec. `false` when the helper isn't
|
||||
/// installed (tarball/old package), pkexec is missing, or polkit denies the action.
|
||||
fn dm_helper(verb: &str) -> bool {
|
||||
let Some(helper) = DM_HELPER_PATHS
|
||||
.iter()
|
||||
.find(|p| std::path::Path::new(p).exists())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
Command::new("pkexec")
|
||||
.arg(helper)
|
||||
.arg(verb)
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Stop the display manager for a takeover on a mask-fragile DM flavor. Plain `systemctl stop` on
|
||||
/// the SYSTEM bus first — succeeds as root or under an operator polkit rule scoped to the DM unit
|
||||
/// (see docs); fails cleanly otherwise ("interactive authentication required") — then the
|
||||
/// packaged pkexec helper. `false` means no privilege path exists and the caller degrades to
|
||||
/// attach.
|
||||
fn try_stop_display_manager(dm: &str) -> bool {
|
||||
let direct = Command::new("systemctl")
|
||||
.args(["stop", dm])
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
direct || dm_helper("stop")
|
||||
}
|
||||
|
||||
/// Restore the display manager: `reset-failed` (a relogin loop may have tripped the unit's start
|
||||
/// limit, and a plain restart is refused until the accounting clears) + `restart` — its autologin
|
||||
/// session Exec brings the box's own session back up. Plain system-bus verbs first (root / an
|
||||
/// operator polkit rule), then the packaged pkexec helper, whose `restore` verb performs the same
|
||||
/// two steps as root.
|
||||
fn restore_display_manager(dm: &str) -> bool {
|
||||
let _ = Command::new("systemctl")
|
||||
.args(["reset-failed", dm])
|
||||
.status();
|
||||
let direct = Command::new("systemctl")
|
||||
.args(["restart", dm])
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
direct || dm_helper("restore")
|
||||
}
|
||||
|
||||
/// The distro's session-switch helper (ChimeraOS/Nobara layout). Its USER pass records the
|
||||
/// sentinel + self-pkexecs (authorized `allow_any` by the distro's own polkit action policy, so
|
||||
/// it works from our sessionless context); its ROOT pass rewrites the DM autologin config — but
|
||||
@@ -1219,10 +1268,13 @@ fn honor_session_select_switch(dm: String) {
|
||||
clear_takeover();
|
||||
*MANAGED_SESSION.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||
stop_session(SESSION_UNIT); // dead already (the switch shut its Steam down) — clear the unit
|
||||
let _ = Command::new("systemctl")
|
||||
.args(["reset-failed", &dm])
|
||||
.status();
|
||||
let _ = Command::new("systemctl").args(["start", &dm]).status();
|
||||
if !restore_display_manager(&dm) {
|
||||
tracing::warn!(
|
||||
%dm,
|
||||
"gamescope: display-manager start was denied — the desktop switch may need a manual \
|
||||
`systemctl restart` of the DM"
|
||||
);
|
||||
}
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while Instant::now() < deadline {
|
||||
let active = Command::new("systemctl")
|
||||
@@ -1343,8 +1395,10 @@ fn stop_autologin_sessions() -> Result<()> {
|
||||
if !try_stop_display_manager(&dm) {
|
||||
bail!(
|
||||
"the box's gaming session is driven by {dm}, which does not survive a masked \
|
||||
session unit, and stopping it needs privilege — install the punktfunk \
|
||||
display-manager polkit rule (see docs) to enable the managed takeover"
|
||||
session unit, and stopping it needs privilege — the packaged pf-dm-helper \
|
||||
polkit action is missing or was denied (reinstall the punktfunk package, or \
|
||||
install the display-manager polkit rule from the docs) so the managed takeover \
|
||||
is unavailable"
|
||||
);
|
||||
}
|
||||
tracing::info!(
|
||||
@@ -1668,14 +1722,7 @@ fn do_restore_tv_session() {
|
||||
// (`reset-failed` clears the relogin start-limit accounting, then `restart`); its autologin
|
||||
// session Exec starts the gamescope unit itself.
|
||||
if let Some(dm) = dm {
|
||||
let _ = Command::new("systemctl")
|
||||
.args(["reset-failed", &dm])
|
||||
.status();
|
||||
let restart = Command::new("systemctl")
|
||||
.args(["restart", &dm])
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
let restart = restore_display_manager(&dm);
|
||||
if restart {
|
||||
tracing::info!(%dm, "restored the display manager (its autologin brings gaming mode back)");
|
||||
} else if crate::try_recover_session() {
|
||||
|
||||
@@ -40,8 +40,16 @@ depends on the display manager driving the autologin:
|
||||
|
||||
- **SDDM** (Bazzite, SteamOS): handled automatically — no setup.
|
||||
- **plasmalogin** (Nobara) and other display managers: the host must stop the display manager
|
||||
itself for the length of the stream and restart it afterwards, which needs privilege. Allow it
|
||||
with a polkit rule (adjust the unit and user names to your box):
|
||||
itself for the length of the stream and restart it afterwards, which needs privilege. The
|
||||
packages ship that privilege: a root helper (`/usr/libexec/punktfunk/pf-dm-helper`) behind its
|
||||
own polkit action (`io.unom.punktfunk.dm-helper`), invoked automatically when the plain
|
||||
`systemctl` verbs are denied — no setup. The helper only stops/restores the unit the
|
||||
`display-manager.service` symlink points at, the same class of local-seat operation these
|
||||
distros already authorize for their own session switcher (Nobara's `os-session-select`).
|
||||
|
||||
Installed from a tarball, or prefer not to ship the `allow_any` action? Remove the `.policy`
|
||||
file and use a polkit rule scoped to your user instead (adjust the unit and user names to your
|
||||
box) — the host tries the plain verbs first, so the rule takes precedence:
|
||||
|
||||
```js
|
||||
// /etc/polkit-1/rules.d/49-punktfunk-dm.rules
|
||||
@@ -54,15 +62,16 @@ depends on the display manager driving the autologin:
|
||||
});
|
||||
```
|
||||
|
||||
Without the rule the host degrades safely: it **attaches** to the live Gaming Mode session
|
||||
instead (Game Mode stays on the box's display, mirrored to the client) rather than risk the
|
||||
display manager. If the display-manager restart ever loses its privilege mid-restore,
|
||||
`PUNKTFUNK_RECOVER_SESSION_CMD` (see [Configuration](/docs/configuration)) is fired as the
|
||||
fallback.
|
||||
With no privilege path at all the host degrades safely: it **attaches** to the live Gaming Mode
|
||||
session instead (Game Mode stays on the box's display at the box's own resolution, mirrored to
|
||||
the client — if your monitor stays on and the stream runs at the desktop's resolution, this is
|
||||
what happened; check the host log for "managed takeover unavailable"). If the display-manager
|
||||
restart ever loses its privilege mid-restore, `PUNKTFUNK_RECOVER_SESSION_CMD` (see
|
||||
[Configuration](/docs/configuration)) is fired as the fallback.
|
||||
|
||||
With the rule in place the **in-stream session switch round-trips** in managed mode: Steam's
|
||||
"Switch to Desktop" inside the streamed Game Mode returns the box to its desktop session and the
|
||||
stream follows it there; the desktop's "Return to Gaming Mode" switches it forward again.
|
||||
With the takeover authorized the **in-stream session switch round-trips** in managed mode:
|
||||
Steam's "Switch to Desktop" inside the streamed Game Mode returns the box to its desktop session
|
||||
and the stream follows it there; the desktop's "Return to Gaming Mode" switches it forward again.
|
||||
|
||||
## Session following
|
||||
|
||||
|
||||
@@ -129,6 +129,14 @@ package_punktfunk-host() {
|
||||
install -Dm0755 "$T/punktfunk-host" "$pkgdir/usr/bin/punktfunk-host"
|
||||
# /dev/uinput + /dev/uhid -> input group (virtual gamepads + DualSense UHID)
|
||||
install -Dm0644 "$R/scripts/60-punktfunk.rules" "$pkgdir/usr/lib/udev/rules.d/60-punktfunk.rules"
|
||||
# Managed gamescope takeover on DM-autologin boxes: root helper + polkit action so the host can
|
||||
# stop/restore the display manager for the stream. Arch has no /usr/libexec — install under
|
||||
# /usr/lib/punktfunk and rewrite the policy's exec.path annotation to match (the host probes both).
|
||||
install -Dm0755 "$R/scripts/pf-dm-helper" "$pkgdir/usr/lib/punktfunk/pf-dm-helper"
|
||||
install -Dm0644 "$R/scripts/io.unom.punktfunk.dm-helper.policy" \
|
||||
"$pkgdir/usr/share/polkit-1/actions/io.unom.punktfunk.dm-helper.policy"
|
||||
sed -i 's#/usr/libexec/punktfunk/pf-dm-helper#/usr/lib/punktfunk/pf-dm-helper#' \
|
||||
"$pkgdir/usr/share/polkit-1/actions/io.unom.punktfunk.dm-helper.policy"
|
||||
# vhci-hcd autoload — usbip transport for the virtual Steam Deck pad (Steam only adopts USB pads)
|
||||
install -Dm0644 "$R/scripts/punktfunk-modules.conf" "$pkgdir/usr/lib/modules-load.d/punktfunk.conf"
|
||||
# 32 MB UDP socket buffers (send-side headroom at high bitrate)
|
||||
|
||||
@@ -52,6 +52,11 @@ SHAREDIR="$STAGE/usr/share/$PKG"
|
||||
# --- file layout (matches the RPM %install) ----------------------------------
|
||||
install -Dm0755 "$BIN" "$STAGE/usr/bin/$PKG"
|
||||
install -Dm0644 scripts/60-punktfunk.rules "$STAGE/usr/lib/udev/rules.d/60-punktfunk.rules"
|
||||
# Managed gamescope takeover on DM-autologin boxes: root helper + polkit action so the host can
|
||||
# stop/restore the display manager for the stream (the helper derives the DM unit itself).
|
||||
install -Dm0755 scripts/pf-dm-helper "$STAGE/usr/libexec/punktfunk/pf-dm-helper"
|
||||
install -Dm0644 scripts/io.unom.punktfunk.dm-helper.policy \
|
||||
"$STAGE/usr/share/polkit-1/actions/io.unom.punktfunk.dm-helper.policy"
|
||||
# vhci-hcd autoload — usbip transport for the virtual Steam Deck pad (Steam only adopts USB pads).
|
||||
install -Dm0644 scripts/punktfunk-modules.conf "$STAGE/usr/lib/modules-load.d/punktfunk.conf"
|
||||
# UDP socket-buffer tuning (32 MB) — without it the kernel clamps the host's SO_SNDBUF to ~416 KB
|
||||
|
||||
@@ -249,6 +249,12 @@ install -Dm0755 target/release/punktfunk-host %{buildroot}%{_bindir}/punktfunk-h
|
||||
# udev rule — /dev/uinput access for virtual gamepads (input group).
|
||||
install -Dm0644 scripts/60-punktfunk.rules %{buildroot}%{_udevrulesdir}/60-punktfunk.rules
|
||||
|
||||
# Managed gamescope takeover on DM-autologin boxes (Nobara's plasmalogin): a root helper + polkit
|
||||
# action let the host stop/restore the display manager for the stream without a hand-installed
|
||||
# polkit rule. The helper derives the DM unit itself — callers can't name arbitrary units.
|
||||
install -Dm0755 scripts/pf-dm-helper %{buildroot}%{_libexecdir}/punktfunk/pf-dm-helper
|
||||
install -Dm0644 scripts/io.unom.punktfunk.dm-helper.policy %{buildroot}%{_datadir}/polkit-1/actions/io.unom.punktfunk.dm-helper.policy
|
||||
|
||||
# vhci-hcd autoload — the usbip transport that makes the virtual Steam Deck controller a
|
||||
# real USB device (Steam Input only adopts those; the UHID fallback is invisible to Steam).
|
||||
install -Dm0644 scripts/punktfunk-modules.conf %{buildroot}%{_prefix}/lib/modules-load.d/punktfunk.conf
|
||||
@@ -383,6 +389,9 @@ install -Dm0644 scripts/punktfunk-scripting.service %{buildroot}%{_userunitdir}/
|
||||
%{_bindir}/punktfunk-host
|
||||
%{_bindir}/punktfunk-tray
|
||||
%{_udevrulesdir}/60-punktfunk.rules
|
||||
%dir %{_libexecdir}/punktfunk
|
||||
%{_libexecdir}/punktfunk/pf-dm-helper
|
||||
%{_datadir}/polkit-1/actions/io.unom.punktfunk.dm-helper.policy
|
||||
%{_prefix}/lib/modules-load.d/punktfunk.conf
|
||||
%{_prefix}/lib/sysctl.d/99-punktfunk-net.conf
|
||||
%{_prefix}/lib/firewalld/services/punktfunk-gamestream.xml
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE policyconfig PUBLIC
|
||||
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
|
||||
<policyconfig>
|
||||
<vendor>Punktfunk</vendor>
|
||||
<vendor_url>https://punktfunk.unom.io</vendor_url>
|
||||
|
||||
<!-- Stop/restore the box's display manager for a managed gamescope takeover. The helper derives
|
||||
the unit from the display-manager.service symlink itself (no caller-chosen units), so this
|
||||
grant is scoped to the box's own local-seat session lifecycle — the same class of operation
|
||||
these distros already authorize for their session switcher (e.g. Nobara's
|
||||
os-session-select, allow_any). allow_any because the host commonly runs sessionless (a
|
||||
lingering user unit, no polkit agent), where interactive auth can never be answered. -->
|
||||
<action id="io.unom.punktfunk.dm-helper">
|
||||
<description>Stop or restore the display manager for a Punktfunk stream</description>
|
||||
<message>Authentication is required to switch the display manager for a Punktfunk stream</message>
|
||||
<defaults>
|
||||
<allow_any>yes</allow_any>
|
||||
<allow_inactive>yes</allow_inactive>
|
||||
<allow_active>yes</allow_active>
|
||||
</defaults>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/libexec/punktfunk/pf-dm-helper</annotate>
|
||||
</action>
|
||||
</policyconfig>
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
# Privileged display-manager verbs for the punktfunk managed gamescope takeover.
|
||||
#
|
||||
# On DM-autologin boxes whose display manager does not survive a masked session unit (Nobara's
|
||||
# plasmalogin, unknown DMs), taking the Gaming Mode session over means stopping the display
|
||||
# manager for the length of the stream and restarting it afterwards — root-only operations. The
|
||||
# host invokes this helper via pkexec under its own polkit action
|
||||
# (io.unom.punktfunk.dm-helper.policy, installed by the packages), the same mechanism these
|
||||
# distros use for their own session switching (Nobara's os-session-select).
|
||||
#
|
||||
# The unit is NEVER caller-controlled: it is derived here from the display-manager.service alias
|
||||
# symlink, so the polkit grant's blast radius is exactly "the box's own display manager" — a
|
||||
# local-seat operation, not arbitrary unit management.
|
||||
set -eu
|
||||
|
||||
dm_unit() {
|
||||
target=$(readlink /etc/systemd/system/display-manager.service) || {
|
||||
echo "pf-dm-helper: no display-manager.service alias — no display manager to manage" >&2
|
||||
exit 1
|
||||
}
|
||||
basename "$target"
|
||||
}
|
||||
|
||||
case "${1-}" in
|
||||
stop)
|
||||
exec systemctl stop "$(dm_unit)"
|
||||
;;
|
||||
restore)
|
||||
dm=$(dm_unit)
|
||||
# reset-failed first: a relogin loop may have tripped the unit's start limit, and a plain
|
||||
# restart would be refused until the accounting is cleared.
|
||||
systemctl reset-failed "$dm" 2>/dev/null || :
|
||||
exec systemctl restart "$dm"
|
||||
;;
|
||||
*)
|
||||
echo "usage: pf-dm-helper stop|restore" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user