Files
punktfunk/crates
enricobuehler 647e2b8891
ci / bun-nix (pull_request) Successful in 29s
ci / web (pull_request) Successful in 1m28s
ci / rust-arm64 (pull_request) Successful in 1m30s
apple / swift (pull_request) Successful in 1m44s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m45s
android / android (pull_request) Successful in 3m49s
ci / rust (pull_request) Successful in 11m12s
fix(pf-vdisplay): bind our wrapper over the hardcoded gamescope path, where GAMESCOPE_BIN cannot reach
Nobara's gamescope-session-plus HARDCODES an absolute gamescope path and never reads GAMESCOPE_BIN —
verified on the box: `grep GAMESCOPE_BIN` over the script returns nothing, and line 244 opens
`GAMESCOPECMD="/usr/bin/gamescope \`. So neither the wrapper nor a PATH shim can reach it, and the
managed session comes up as stock gamescope with none of our flags. `verify_managed_spawn_flags`
catches that and refuses HDR and the in-node cursor rather than streaming a session planned around
flags that never arrived — correct, but it leaves every Nobara-family box with no composited cursor
and no HDR, and the operator's only recourse is overwriting a distro-owned binary.

Bind it instead, inside the session unit's own mount namespace: the session gets our gamescope, and
nothing outside the unit changes — the distro still owns the file on disk.

⚠ The bind source is the WRAPPER, not the patched binary. Binding the binary would have been a
vacuous fix: the flags this whole mechanism exists to deliver are injected BY the wrapper, so a bound
binary arrives with no flags and `verify_managed_spawn_flags` refuses exactly as before. Binding the
wrapper reproduces what GAMESCOPE_BIN would have done had the script consulted it.

Applied only where the escape hatch is structurally absent, decided by READING the script rather than
by distro name: if `GAMESCOPE_BIN` appears anywhere in it, we keep the mechanism that already works
and take no mount namespace we don't need. Parsing is pure and unit-tested — the `GAMESCOPECMD+=`
appends and the `[ -z "$GAMESCOPECMD" ]` test must not be mistaken for the opening assignment, or the
bind would land over `$socket`. Three further refusals: the target must be a file; the resolved
source must be absolute and NOT the target (a fork-bomb guard — the wrapper execs the real binary, so
shadowing that same path would exec itself forever); and the preflight must pass.

Availability is PROVEN, not inferred, with a throwaway `systemd-run --user --wait --collect
--property=BindReadOnlyPaths=<the real value> -- /bin/true`. That matters: on .25 the bind fails with
`status=226/NAMESPACE` while `kernel.unprivileged_userns_clone=1` and `max_user_namespaces=29006` are
both permissive — AppArmor's `apparmor_restrict_unprivileged_userns` blocks it. A sysctl check would
have said "available" and armed a bind that cannot work. The payload is /bin/true, so the preflight
can never accidentally start a compositor.

Runtime backstop for anything the preflight cannot see: `ExecMainStatus == 226` disarms the bind and
relaunches plain, latching one-way per process.

The transient host-owned unit takes the setting via `systemd-run --property=`, which is atomic with
the start and leaves nothing behind if the host dies. The box's own unit needs a drop-in, so it gets
`zz-punktfunk-bind.conf` on the TEMPLATE, sorted last so nothing can take the setting back out;
removal targets that one filename only — never the directory, never a glob — because the operator's
`10-headless.conf` next to it is what lets game mode start on that box at all.
2026-08-09 23:11:00 +02:00
..