Field reports on 0.26.0-1, CachyOS on NVIDIA and AMD, and Bazzite — surviving a clean reinstall of host and client:
KWin virtual output failed: KWin does not expose zkde_screencast_unstable_v1 to this client
Worked around only by KWIN_WAYLAND_NO_PERMISSION_CHECKS=1.
Cause
0.26.0-1 granted the host CAP_SYS_NICE for the PyroWave GPU-priority lever. That capability and the KWin desktop backend are mutually exclusive at the kernel level.
KWin advertises its restricted protocols — zkde_screencast_unstable_v1 (our virtual output) and org_kde_kwin_fake_input (input injection) — only to a client it can identify:
executablePath() is QFileInfo("/proc/<pid>/exe").symLinkTarget() (src/utils/executable_path_proc.cpp)
the result is matched against an installed .desktop's Exec= (src/utils/serviceutils.h), empty list on no match
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 a capability-carrying host is unidentifiable: executablePath() empty → nothing matches → globals never advertised → 8 retries → dead session. It presents exactly as a missing .desktop, which is why reinstalling does nothing.
KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 bypasses precisely this check and nothing else, which is why it "fixed" it in the field.
Why the obvious fixes don't work
Measured on CachyOS kernel 7.1.6, same-uid reader, cap_sys_nice=ep on the target:
target process
readlink /proc/<pid>/exe
no capability
✅ OK
capability
❌ EPERM
capability + prctl(PR_SET_DUMPABLE, 1)
❌ EPERM — dumpable is not the gate
capability dropped + PR_SET_DUMPABLE(1)
✅ OK — needs both
Row 3 is also the systemd AmbientCapabilities= case (CapPrm set, dumpable 1), so moving the grant to the unit file fails identically. Only an uncapped process is identifiable.
It was in five channels, not one
channel
how it granted
reach
packaging/arch/punktfunk-host.install
setcap in post_install/post_upgrade
Arch, CachyOS
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
Bazzite sysext
packaging/debian/build-deb.sh
setcap in postinst
Debian/Ubuntu
packaging/nix/nixos-module.nix
security.wrappers + capabilities=
NixOS
scripts/steamdeck/install.sh
setcap "$BIN"
Steam Deck — voided the .desktop written six lines earlier
Bazzite is this bug, not a separate one. Verified by mounting the published punktfunk-0.26.0-1-x86-64.raw: getcap usr/bin/punktfunk-host → cap_sys_nice=ep, stored as security.capability in the squashfs. The comment in packaging/arch/build-sysext.sh claiming "file capabilities don't survive this squashfs path" is false — mksquashfs records them — and is corrected here.
NixOS does not dodge it either. A security.wrappers entry raises the capability into its ambient set before exec'ing the store binary, precisely so it survives — landing CAP_SYS_NICE in the exec'd process's permitted set. 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.
What this costs
Pacing only. pf-zerocopy already walks REALTIME → HIGH → default when a priority class is refused, and pf-frame's thread nice is a documented best-effort no-op. This is exactly 0.25.0's behaviour — the behaviour that worked.
Changes
All six grant sites removed. Arch/deb/Deck actively setcap -r so boxes that already ran 0.26.0-1's scriptlet get healed; rpm applies caps from package metadata, so a package built without %caps() clears it on upgrade.
The sysext build now hard-fails if a capability is staged. A merged sysext's /usr is read-only squashfs — a bad image cannot be repaired on the box, and the spec was one %caps() away from baking one in again.
pf-vdisplay kwin.rs: all three "does not expose zkde_screencast" errors now read /proc/self/status and, if the process holds any capability, name it with its CapPrm mask and the setcap -r that repairs it. This failure is undiagnosable from the Wayland side, and a hand-rolled setcap reproduces it independently of our packaging.
Docs corrected — they advertised the capability as a feature: running-as-a-service §"GPU scheduling priority" rewritten, configuration.md's PYROWAVE_QUEUE_PRIORITY row, packaging/bazzite/README.md §6.5 (which still described kde-desktop-setup.shwriting the env var it now removes), packaging/arch/README.md, and the v0.26.0 CHANGELOG entry annotated with the correction rather than rewritten.
Compositor audit
Measured blast radius of holding a capability (same-uid reader): /proc/PID/exeEPERM, /proc/PID/root/*EPERM, /proc/PID/environEPERM; cgroup, status, cmdline still readable.
KWin — broken outright. Proven, field-confirmed.
gamescope — no identity gate, unaffected. Matches the field: only Desktop mode was ever reported.
Mutter — drives Mutter's own D-Bus API, not the portal. Looks unaffected.
wlroots / Hyprland — go through the ScreenCast portal, whose app-id resolution reads /proc/<pid>/root/.flatpak-info, a path the capability blocks. A real exposure, not reproduced end to end — flagging honestly rather than claiming it.
Verification
On 192.168.1.21 (CachyOS 7.1.6):
the capability/dumpable matrix above, and the /proc read matrix
2 new unit tests pass; the diagnostic hint exercised end-to-end — silent uncapped, firing with CapPrm=0x0000000000800000 under cap_sys_nice=ep
shipped punktfunk-host-0.26.0-1-x86_64.pkg.tar.zst unpacked, .INSTALL confirmed to carry the setcap on both post_install and post_upgrade
published punktfunk-0.26.0-1-x86-64.raw mounted, capability read out of the squashfs
bash -n on all five changed shell files; nix-instantiate --parse on nixos-module.nix and packages.nix
getcap on an uncapped file exits 0 with empty output, so the new sysext assertion cannot false-positive
Shipping
Ships as 0.26.0-2 — packaging, docs, one crate; no version bump. Same pkgrel-repair shape as the FFmpeg soname break.
⚠ Two things worth deciding:
Bazzite users on 0.26.0-1 cannot be repaired in place (read-only /usr) — they need a rebuilt image, so the sysext feed has to be republished, not just the packages.
I'd hold the 0.26.0 announcement until -2 is out, since -1 bricks desktop streaming on every KDE box.
Merging is yours to click.
Field reports on 0.26.0-1, CachyOS on NVIDIA **and** AMD, and Bazzite — surviving a clean reinstall of host and client:
```
KWin virtual output failed: KWin does not expose zkde_screencast_unstable_v1 to this client
```
Worked around only by `KWIN_WAYLAND_NO_PERMISSION_CHECKS=1`.
## Cause
0.26.0-1 granted the host `CAP_SYS_NICE` for the PyroWave GPU-priority lever. **That capability and the KWin desktop backend are mutually exclusive at the kernel level.**
KWin advertises its restricted protocols — `zkde_screencast_unstable_v1` (our virtual output) and `org_kde_kwin_fake_input` (input injection) — only to a client it can *identify*:
1. `KWinDisplay::allowInterface()` → `fetchRequestedInterfaces(client->executablePath())` (Plasma/6.5 `src/wayland_server.cpp`)
2. `executablePath()` is `QFileInfo("/proc/<pid>/exe").symLinkTarget()` (`src/utils/executable_path_proc.cpp`)
3. the result is matched against an installed `.desktop`'s `Exec=` (`src/utils/serviceutils.h`), empty list on no match
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 a capability-carrying host is unidentifiable: `executablePath()` empty → nothing matches → globals never advertised → 8 retries → dead session. It presents exactly as a missing `.desktop`, which is why reinstalling does nothing.
`KWIN_WAYLAND_NO_PERMISSION_CHECKS=1` bypasses precisely this check and nothing else, which is why it "fixed" it in the field.
## Why the obvious fixes don't work
Measured on CachyOS kernel 7.1.6, same-uid reader, `cap_sys_nice=ep` on the target:
| target process | `readlink /proc/<pid>/exe` |
|---|---|
| no capability | ✅ OK |
| capability | ❌ EPERM |
| capability + `prctl(PR_SET_DUMPABLE, 1)` | ❌ EPERM — **dumpable is not the gate** |
| capability dropped + `PR_SET_DUMPABLE(1)` | ✅ OK — needs **both** |
Row 3 is also the systemd `AmbientCapabilities=` case (CapPrm set, dumpable 1), so moving the grant to the unit file fails identically. Only an uncapped process is identifiable.
## It was in five channels, not one
| channel | how it granted | reach |
|---|---|---|
| `packaging/arch/punktfunk-host.install` | `setcap` in post_install/post_upgrade | Arch, CachyOS |
| `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 | Bazzite sysext |
| `packaging/debian/build-deb.sh` | `setcap` in postinst | Debian/Ubuntu |
| `packaging/nix/nixos-module.nix` | `security.wrappers` + `capabilities=` | NixOS |
| `scripts/steamdeck/install.sh` | `setcap "$BIN"` | Steam Deck — voided the `.desktop` written six lines earlier |
**Bazzite is this bug, not a separate one.** Verified by mounting the published `punktfunk-0.26.0-1-x86-64.raw`: `getcap usr/bin/punktfunk-host` → `cap_sys_nice=ep`, stored as `security.capability` in the squashfs. The comment in `packaging/arch/build-sysext.sh` claiming *"file capabilities don't survive this squashfs path"* is false — mksquashfs records them — and is corrected here.
**NixOS does not dodge it either.** A `security.wrappers` entry raises the capability into its *ambient* set before exec'ing the store binary, precisely so it survives — landing `CAP_SYS_NICE` in the exec'd process's permitted set. `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.
## What this costs
Pacing only. `pf-zerocopy` already walks REALTIME → HIGH → default when a priority class is refused, and `pf-frame`'s thread nice is a documented best-effort no-op. This is exactly 0.25.0's behaviour — the behaviour that worked.
## Changes
- **All six grant sites removed.** Arch/deb/Deck actively `setcap -r` so boxes that already ran 0.26.0-1's scriptlet get healed; rpm applies caps from package metadata, so a package built without `%caps()` clears it on upgrade.
- **The sysext build now hard-fails** if a capability is staged. A merged sysext's `/usr` is read-only squashfs — a bad image cannot be repaired on the box, and the spec was one `%caps()` away from baking one in again.
- **`pf-vdisplay` kwin.rs**: all three "does not expose zkde_screencast" errors now read `/proc/self/status` and, if the process holds any capability, name it with its `CapPrm` mask and the `setcap -r` that repairs it. This failure is undiagnosable from the Wayland side, and a hand-rolled `setcap` reproduces it independently of our packaging.
- **Docs corrected** — they advertised the capability as a feature: `running-as-a-service` §"GPU scheduling priority" rewritten, `configuration.md`'s `PYROWAVE_QUEUE_PRIORITY` row, `packaging/bazzite/README.md` §6.5 (which still described `kde-desktop-setup.sh` *writing* the env var it now *removes*), `packaging/arch/README.md`, and the v0.26.0 CHANGELOG entry annotated with the correction rather than rewritten.
## Compositor audit
Measured blast radius of holding a capability (same-uid reader): `/proc/PID/exe` **EPERM**, `/proc/PID/root/*` **EPERM**, `/proc/PID/environ` **EPERM**; `cgroup`, `status`, `cmdline` still readable.
- **KWin** — broken outright. Proven, field-confirmed.
- **gamescope** — no identity gate, unaffected. Matches the field: only Desktop mode was ever reported.
- **Mutter** — drives Mutter's own D-Bus API, not the portal. Looks unaffected.
- **wlroots / Hyprland** — go through the ScreenCast portal, whose app-id resolution reads `/proc/<pid>/root/.flatpak-info`, a path the capability blocks. A real exposure, **not** reproduced end to end — flagging honestly rather than claiming it.
## Verification
On 192.168.1.21 (CachyOS 7.1.6):
- the capability/dumpable matrix above, and the `/proc` read matrix
- `cargo check -p pf-vdisplay --all-targets` and `cargo clippy -p pf-vdisplay --all-targets -- -D warnings` clean
- 2 new unit tests pass; the diagnostic hint exercised end-to-end — silent uncapped, firing with `CapPrm=0x0000000000800000` under `cap_sys_nice=ep`
- shipped `punktfunk-host-0.26.0-1-x86_64.pkg.tar.zst` unpacked, `.INSTALL` confirmed to carry the setcap on both `post_install` and `post_upgrade`
- published `punktfunk-0.26.0-1-x86-64.raw` mounted, capability read out of the squashfs
- `bash -n` on all five changed shell files; `nix-instantiate --parse` on `nixos-module.nix` and `packages.nix`
- `getcap` on an uncapped file exits 0 with empty output, so the new sysext assertion cannot false-positive
## Shipping
Ships as **0.26.0-2** — packaging, docs, one crate; no version bump. Same pkgrel-repair shape as the FFmpeg soname break.
⚠ Two things worth deciding:
- **Bazzite users on 0.26.0-1 cannot be repaired in place** (read-only `/usr`) — they need a rebuilt image, so the sysext feed has to be republished, not just the packages.
- I'd hold the 0.26.0 announcement until `-2` is out, since `-1` bricks desktop streaming on every KDE box.
Merging is yours to click.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Field reports on 0.26.0-1, CachyOS on NVIDIA and AMD, and Bazzite — surviving a clean reinstall of host and client:
Worked around only by
KWIN_WAYLAND_NO_PERMISSION_CHECKS=1.Cause
0.26.0-1 granted the host
CAP_SYS_NICEfor the PyroWave GPU-priority lever. That capability and the KWin desktop backend are mutually exclusive at the kernel level.KWin advertises its restricted protocols —
zkde_screencast_unstable_v1(our virtual output) andorg_kde_kwin_fake_input(input injection) — only to a client it can identify:KWinDisplay::allowInterface()→fetchRequestedInterfaces(client->executablePath())(Plasma/6.5src/wayland_server.cpp)executablePath()isQFileInfo("/proc/<pid>/exe").symLinkTarget()(src/utils/executable_path_proc.cpp).desktop'sExec=(src/utils/serviceutils.h), empty list on no matchThe 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 a capability-carrying host is unidentifiable:executablePath()empty → nothing matches → globals never advertised → 8 retries → dead session. It presents exactly as a missing.desktop, which is why reinstalling does nothing.KWIN_WAYLAND_NO_PERMISSION_CHECKS=1bypasses precisely this check and nothing else, which is why it "fixed" it in the field.Why the obvious fixes don't work
Measured on CachyOS kernel 7.1.6, same-uid reader,
cap_sys_nice=epon the target:readlink /proc/<pid>/exeprctl(PR_SET_DUMPABLE, 1)PR_SET_DUMPABLE(1)Row 3 is also the systemd
AmbientCapabilities=case (CapPrm set, dumpable 1), so moving the grant to the unit file fails identically. Only an uncapped process is identifiable.It was in five channels, not one
packaging/arch/punktfunk-host.installsetcapin post_install/post_upgradepackaging/rpm/punktfunk.spec%caps(cap_sys_nice=ep)in%filespackaging/bazzite/build-sysext.shsetcapon the staging treepackaging/debian/build-deb.shsetcapin postinstpackaging/nix/nixos-module.nixsecurity.wrappers+capabilities=scripts/steamdeck/install.shsetcap "$BIN".desktopwritten six lines earlierBazzite is this bug, not a separate one. Verified by mounting the published
punktfunk-0.26.0-1-x86-64.raw:getcap usr/bin/punktfunk-host→cap_sys_nice=ep, stored assecurity.capabilityin the squashfs. The comment inpackaging/arch/build-sysext.shclaiming "file capabilities don't survive this squashfs path" is false — mksquashfs records them — and is corrected here.NixOS does not dodge it either. A
security.wrappersentry raises the capability into its ambient set before exec'ing the store binary, precisely so it survives — landingCAP_SYS_NICEin the exec'd process's permitted set.ExecStartnow points at the store path directly, which is also the pathpackages.nixsubstitutes into the.desktop'sExec=, so the two finally agree.What this costs
Pacing only.
pf-zerocopyalready walks REALTIME → HIGH → default when a priority class is refused, andpf-frame's thread nice is a documented best-effort no-op. This is exactly 0.25.0's behaviour — the behaviour that worked.Changes
setcap -rso boxes that already ran 0.26.0-1's scriptlet get healed; rpm applies caps from package metadata, so a package built without%caps()clears it on upgrade./usris read-only squashfs — a bad image cannot be repaired on the box, and the spec was one%caps()away from baking one in again.pf-vdisplaykwin.rs: all three "does not expose zkde_screencast" errors now read/proc/self/statusand, if the process holds any capability, name it with itsCapPrmmask and thesetcap -rthat repairs it. This failure is undiagnosable from the Wayland side, and a hand-rolledsetcapreproduces it independently of our packaging.running-as-a-service§"GPU scheduling priority" rewritten,configuration.md'sPYROWAVE_QUEUE_PRIORITYrow,packaging/bazzite/README.md§6.5 (which still describedkde-desktop-setup.shwriting the env var it now removes),packaging/arch/README.md, and the v0.26.0 CHANGELOG entry annotated with the correction rather than rewritten.Compositor audit
Measured blast radius of holding a capability (same-uid reader):
/proc/PID/exeEPERM,/proc/PID/root/*EPERM,/proc/PID/environEPERM;cgroup,status,cmdlinestill readable./proc/<pid>/root/.flatpak-info, a path the capability blocks. A real exposure, not reproduced end to end — flagging honestly rather than claiming it.Verification
On 192.168.1.21 (CachyOS 7.1.6):
/procread matrixcargo check -p pf-vdisplay --all-targetsandcargo clippy -p pf-vdisplay --all-targets -- -D warningscleanCapPrm=0x0000000000800000undercap_sys_nice=eppunktfunk-host-0.26.0-1-x86_64.pkg.tar.zstunpacked,.INSTALLconfirmed to carry the setcap on bothpost_installandpost_upgradepunktfunk-0.26.0-1-x86-64.rawmounted, capability read out of the squashfsbash -non all five changed shell files;nix-instantiate --parseonnixos-module.nixandpackages.nixgetcapon an uncapped file exits 0 with empty output, so the new sysext assertion cannot false-positiveShipping
Ships as 0.26.0-2 — packaging, docs, one crate; no version bump. Same pkgrel-repair shape as the FFmpeg soname break.
⚠ Two things worth deciding:
/usr) — they need a rebuilt image, so the sysext feed has to be republished, not just the packages.-2is out, since-1bricks desktop streaming on every KDE box.Merging is yours to click.
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.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.