ci / bun-nix (pull_request) Successful in 17s
ci / web (pull_request) Successful in 1m7s
apple / swift (pull_request) Successful in 1m38s
ci / rust-arm64 (pull_request) Successful in 1m38s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m46s
android / android (pull_request) Successful in 5m31s
ci / rust (pull_request) Failing after 9m2s
nix / flake (pull_request) Successful in 12m24s
The Arch fix in the previous commit was incomplete. 0.26.0-1 granted the host CAP_SYS_NICE through
every Linux channel we ship, and each one breaks KWin identification the same way:
* packaging/rpm/punktfunk.spec .......... %caps(cap_sys_nice=ep) in %files <- Fedora AND Bazzite
via rpm-ostree layering
* packaging/bazzite/build-sysext.sh ..... setcap on the staging tree, recorded by mksquashfs
* packaging/debian/build-deb.sh ......... setcap in the postinst
* packaging/nix/nixos-module.nix ........ security.wrappers with capabilities = "cap_sys_nice=ep"
* scripts/steamdeck/install.sh .......... setcap on $BIN, six lines after writing the .desktop
whose Exec= it thereby voids
Bazzite was NOT a separate fault, as first reported here — it is this one. Verified by mounting the
published punktfunk-0.26.0-1-x86-64.raw: `getcap usr/bin/punktfunk-host` reports cap_sys_nice=ep,
stored as security.capability in the squashfs. The claim in packaging/arch/build-sysext.sh that
"file capabilities don't survive this squashfs path" is false and is corrected here; mksquashfs
records them, which is exactly why the image shipped one.
NixOS deserves its own note: a security.wrappers entry does not dodge the problem. The wrapper
raises the capability into its AMBIENT set before exec'ing the store binary, precisely so it
survives — which lands CAP_SYS_NICE in the exec'd process's permitted set and fails the readlink
identically to a file capability. ExecStart now points at the store path directly, which is also the
path packages.nix substitutes into the .desktop's Exec=, so the two finally agree.
Measured blast radius of holding a capability, same-uid reader, CachyOS kernel 7.1.6:
/proc/PID/exe ....... EPERM <- KWin's identification. Desktop sessions die.
/proc/PID/root/* .... EPERM <- xdg-desktop-portal reads .flatpak-info here to resolve an
app id; the wlroots and Hyprland backends go through it
/proc/PID/environ ... EPERM
/proc/PID/cgroup .... OK
/proc/PID/status .... OK
/proc/PID/cmdline ... OK
Compositor backends, by exposure: KWin is broken outright (proven, field-confirmed). gamescope has
no identity gate and was never affected, which matches the field — only Desktop mode was reported.
Mutter drives Mutter's own D-Bus API, not the portal, and looks unaffected. wlroots and Hyprland go
through the ScreenCast portal, whose app-id resolution reads a path the capability blocks — a real
exposure, not something I reproduced end to end.
The sysext build now HARD-FAILS if a capability is staged, rather than trusting that the RPM payload
never carries one: a merged sysext's /usr is read-only squashfs, so a bad image cannot be repaired
on the box, and the spec was one %caps() away from baking one in again.
Docs corrected, because they advertised the capability as a feature:
* docs-site running-as-a-service "GPU scheduling priority" — rewritten: the host carries no
capability, why it must not, and how to clear a 0.26.0-1 install (Bazzite needs a new image)
* docs-site configuration.md — the PYROWAVE_QUEUE_PRIORITY row no longer claims the packages grant it
* packaging/bazzite/README.md — §6.5 still described the kde-desktop-setup.sh behaviour from
before it stopped writing KWIN_WAYLAND_NO_PERMISSION_CHECKS and started REMOVING it; plus a
note that 0.26.0-1 Desktop mode cannot be repaired in place
* packaging/arch/README.md — the false "capabilities don't survive the sysext" line
* CHANGELOG v0.26.0 PW1 — annotated with the 0.26.0-2 correction rather than rewritten, and the
owed PyroWave-under-load A/B now says it needs a gamescope-only box
Verified: bash -n on all five changed shell files; nix-instantiate --parse on nixos-module.nix and
packages.nix; the published 0.26.0-1 sysext mounted and its capability read; getcap on an uncapped
file exits 0 with empty output, so the new build assertion cannot false-positive.
93 lines
4.9 KiB
Bash
Executable File
93 lines
4.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Wrap a built punktfunk pacman package into a systemd-sysext image — the update-survivable way to
|
|
# add it to an immutable Arch-derived distro (SteamOS 3): the .raw overlays /usr read-only from the
|
|
# writable /var/lib/extensions/, so it persists across A/B OS updates with no `steamos-readonly
|
|
# disable`. Works for either split package — on a Steam Deck you'd wrap the CLIENT. Needs
|
|
# `bsdtar`/`tar`, `squashfs-tools` (mksquashfs).
|
|
#
|
|
# Usage: bash build-sysext.sh [--gamescope <punktfunk-gamescope-*.pkg.tar.zst>] \
|
|
# <punktfunk-{host,client}-*.pkg.tar.zst>
|
|
# Output: <pkgname>.raw (e.g. punktfunk-client.raw)
|
|
#
|
|
# --gamescope folds the HDR-capable gamescope companion package (packaging/gamescope) into a HOST
|
|
# image as /usr/bin/punktfunk-gamescope — what lets the gamescope backend stream 10-bit BT.2020 PQ
|
|
# instead of 8-bit SDR (the host prefers that name on PATH and attempts HDR by default). Mirrors
|
|
# the Bazzite image's fold-in, including the honesty check: the binary is verified by executing
|
|
# its `+pfhdr` banner, never trusted by filename. Omit it and the image is exactly what it was —
|
|
# the host then stays SDR on that backend, by design.
|
|
#
|
|
# No CAP_SYS_NICE inside the image, for either binary. ⚠ NOT because capabilities are lost on the
|
|
# way in — that was this comment's earlier claim and it is false: mksquashfs records
|
|
# security.capability, and the published Bazzite 0.26.0-1 image really did carry `cap_sys_nice=ep`
|
|
# on usr/bin/punktfunk-host. It is left out on purpose. A capability on the HOST binary makes it
|
|
# unidentifiable to KWin (which resolves a client's /proc/<pid>/exe to match it against a .desktop,
|
|
# and cannot read it for a capability-carrying process) and kills every Desktop-mode session — see
|
|
# packaging/bazzite/build-sysext.sh, which now hard-fails if one is staged. `punktfunk-gamescope`
|
|
# is a compositor, not a KWin client, so it is unaffected by that rule and simply runs without the
|
|
# capability here, pacing slightly worse.
|
|
set -euo pipefail
|
|
|
|
GAMESCOPE=""
|
|
if [ "${1:-}" = "--gamescope" ]; then
|
|
GAMESCOPE="${2:?--gamescope needs a punktfunk-gamescope package}"; shift 2
|
|
fi
|
|
# No braces in the message: a literal `}` inside ${1:?...} terminates the expansion early and
|
|
# corrupts $PKG (the tail of the message gets appended to the value — a real field bug).
|
|
PKG="${1:?usage: build-sysext.sh [--gamescope <pkg>] <punktfunk-host|client pkg.tar.zst>}"
|
|
[ -f "$PKG" ] || { echo "no such package: $PKG" >&2; exit 1; }
|
|
# Derive the package name from the file (pkgname is everything before the -<version>).
|
|
NAME="$(basename "$PKG" | sed -E 's/-[0-9].*//')"
|
|
[ -n "$NAME" ] || { echo "could not derive package name from $PKG" >&2; exit 1; }
|
|
if [ -n "$GAMESCOPE" ] && [ "$NAME" != "punktfunk-host" ]; then
|
|
echo "--gamescope only makes sense for a punktfunk-host image (got: $NAME)" >&2; exit 1
|
|
fi
|
|
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "$STAGE"' EXIT
|
|
|
|
# A pacman package is a (zstd) tarball; a sysext only carries /usr (the host /etc, /var are the
|
|
# system's). Extract just usr/ from the payload.
|
|
if command -v bsdtar >/dev/null 2>&1; then
|
|
bsdtar -C "$STAGE" -xf "$PKG" usr
|
|
else
|
|
tar -C "$STAGE" -xf "$PKG" usr
|
|
fi
|
|
|
|
# The HDR gamescope companion (see --gamescope in the header). Verified by its banner marker
|
|
# rather than trusted by filename: an unpatched gamescope shipped under this name would make the
|
|
# host promise HDR it cannot deliver, and the punktfunk/1 Welcome cannot take that back
|
|
# mid-session. Executing the staged binary needs a build box the binary runs on (the Arch CI
|
|
# container qualifies; it built it).
|
|
if [ -n "$GAMESCOPE" ]; then
|
|
[ -f "$GAMESCOPE" ] || { echo "no such package: $GAMESCOPE" >&2; exit 1; }
|
|
if command -v bsdtar >/dev/null 2>&1; then
|
|
bsdtar -C "$STAGE" -xf "$GAMESCOPE" usr
|
|
else
|
|
tar -C "$STAGE" -xf "$GAMESCOPE" usr
|
|
fi
|
|
GS_BIN="$STAGE/usr/bin/punktfunk-gamescope"
|
|
[ -x "$GS_BIN" ] || { echo "$GAMESCOPE did not provide usr/bin/punktfunk-gamescope" >&2; exit 1; }
|
|
"$GS_BIN" --version 2>&1 | grep -q '+pfhdr' || {
|
|
echo "$GAMESCOPE's binary has no +pfhdr marker — it is not a punktfunk HDR build" >&2; exit 1; }
|
|
echo "folded in $("$GS_BIN" --version 2>&1 | head -1)"
|
|
fi
|
|
|
|
# The marker systemd-sysext requires to merge the image. ID=_any merges onto ANY host os-release
|
|
# (SteamOS, Arch, Bazzite); ARCHITECTURE pins it to x86-64 so it's never merged on the wrong arch.
|
|
install -d "$STAGE/usr/lib/extension-release.d"
|
|
cat > "$STAGE/usr/lib/extension-release.d/extension-release.$NAME" <<EOF
|
|
ID=_any
|
|
ARCHITECTURE=x86-64
|
|
EOF
|
|
|
|
OUT="$NAME.raw"
|
|
rm -f "$OUT"
|
|
mksquashfs "$STAGE" "$OUT" -all-root -noappend -quiet
|
|
echo "built $OUT"
|
|
echo " install: sudo cp $OUT /var/lib/extensions/ && sudo systemctl enable --now systemd-sysext"
|
|
if [ "$NAME" = "punktfunk-host" ]; then
|
|
echo " then: systemctl --user enable --now punktfunk-host"
|
|
else
|
|
echo " then: run 'punktfunk-client' (or let the Decky plugin launch it)"
|
|
fi
|