Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0464e7407 | ||
|
|
4d383811c0 | ||
|
|
42ee6c5628 | ||
|
|
55f361cb92 | ||
|
|
2079411f4f | ||
|
|
4d1a1348c0 |
+16
-2
@@ -290,7 +290,19 @@ same shader cores a game saturates; NVENC is immune because it has its own ASIC.
|
|||||||
ladder REALTIME → HIGH → no-priority, stepping only on refusal; a refused class can never fail the
|
ladder REALTIME → HIGH → no-priority, stepping only on refusal; a refused class can never fail the
|
||||||
open. The extension probe reuses the `dev_ext_props` already fetched for `queue_family_foreign` and
|
open. The extension probe reuses the `dev_ext_props` already fetched for `queue_family_foreign` and
|
||||||
takes KHR or the EXT alias — the same spelling pf-zerocopy probes, so the two cannot disagree.
|
takes KHR or the EXT alias — the same spelling pf-zerocopy probes, so the two cannot disagree.
|
||||||
⭐ **Needs `CAP_SYS_NICE`**, which the packaging now grants; without it the lever does nothing.
|
⭐ **Needs `CAP_SYS_NICE`**, which the packaging granted in `0.26.0-1`; without it the lever does
|
||||||
|
nothing.
|
||||||
|
🛑 **Corrected in `0.26.0-2`: the packaging no longer grants it, and must not.** Every channel that
|
||||||
|
did (Arch `.install`, RPM `%caps()`, the Bazzite sysext image, the deb postinst, the NixOS
|
||||||
|
`security.wrappers` entry) broke desktop streaming on KDE outright — field-reported on CachyOS and
|
||||||
|
Bazzite as `KWin does not expose zkde_screencast_unstable_v1 to this client`. KWin identifies a
|
||||||
|
client by resolving its `/proc/<pid>/exe` against an installed `.desktop`, and 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`) — KWin has no capabilities, so a capability-carrying host is
|
||||||
|
unidentifiable and the restricted globals are never advertised. Neither `prctl(PR_SET_DUMPABLE, 1)`
|
||||||
|
nor systemd `AmbientCapabilities=` rescues it; only an uncapped process is identifiable. The lever
|
||||||
|
therefore stays wired but unexercised on a stock install (the ladder degrades to default priority),
|
||||||
|
and is opt-in for gamescope-only hosts, which have no such identity check.
|
||||||
- **PW5 — two encoder handles.** `Encoder::Impl` owns exactly one each of `wavelet_img_high_res`,
|
- **PW5 — two encoder handles.** `Encoder::Impl` owns exactly one each of `wavelet_img_high_res`,
|
||||||
`bucket_buffer`, `meta_buffer`, `block_stat_buffer`, `payload_data`, `quant_buffer`, and
|
`bucket_buffer`, `meta_buffer`, `block_stat_buffer`, `payload_data`, `quant_buffer`, and
|
||||||
`Impl::encode` *opens* by discarding them (an image barrier with `VK_IMAGE_LAYOUT_UNDEFINED` as the
|
`Impl::encode` *opens* by discarding them (an image barrier with `VK_IMAGE_LAYOUT_UNDEFINED` as the
|
||||||
@@ -414,7 +426,9 @@ emulator itself would land it outside both.
|
|||||||
|
|
||||||
⏳ **Owed on glass:** iPhone + Bluetooth listen, Apple TV stats overlay, MacBook audio listen, the
|
⏳ **Owed on glass:** iPhone + Bluetooth listen, Apple TV stats overlay, MacBook audio listen, the
|
||||||
Deck HEVC/4:4:4 retest, a Windows wake-from-sleep cycle, and the PyroWave-under-game-load A/B on a
|
Deck HEVC/4:4:4 retest, a Windows wake-from-sleep cycle, and the PyroWave-under-game-load A/B on a
|
||||||
Linux host with `CAP_SYS_NICE` actually granted — the number this whole wave is aimed at.
|
Linux host with `CAP_SYS_NICE` actually granted — the number this whole wave is aimed at. ⚠ That
|
||||||
|
last one now needs a **gamescope-only** host, or a hand-granted capability on a box you are not
|
||||||
|
streaming the KDE desktop from: see the `0.26.0-2` correction under PW1 above.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ pub(crate) fn stamp_color_bits(bitstream: &mut [u8], seq_offset: usize, bt2020_p
|
|||||||
/// repeated value is read as more blocks of the same frame. That is why PW5's alternating encoder
|
/// repeated value is read as more blocks of the same frame. That is why PW5's alternating encoder
|
||||||
/// handles need `pyrowave_encoder_set_next_sequence`, and why a test asserts this reader sees
|
/// handles need `pyrowave_encoder_set_next_sequence`, and why a test asserts this reader sees
|
||||||
/// +1 mod 8 across the pair.
|
/// +1 mod 8 across the pair.
|
||||||
|
///
|
||||||
|
/// Its only caller is the Linux backend — alternating encoder handles are a Linux-side concern, and
|
||||||
|
/// the Windows backend drives pyrowave's compat device with a single handle. The rest of this module
|
||||||
|
/// really is shared (`packet_boundary` and `stamp_color_bits` have callers on both), so the exemption
|
||||||
|
/// is scoped to this one item rather than the file: `dead_code` stays live on Linux, where the caller
|
||||||
|
/// lives and where its disappearing would be a real finding. Windows builds with `-D warnings`, so
|
||||||
|
/// without this the host and tray clippy legs fail to compile the lib at all.
|
||||||
|
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||||
pub(crate) fn wire_sequence(bitstream: &[u8], packet_offset: usize) -> Option<u8> {
|
pub(crate) fn wire_sequence(bitstream: &[u8], packet_offset: usize) -> Option<u8> {
|
||||||
let lo = *bitstream.get(packet_offset + 2)?;
|
let lo = *bitstream.get(packet_offset + 2)?;
|
||||||
let hi = *bitstream.get(packet_offset + 3)?;
|
let hi = *bitstream.get(packet_offset + 3)?;
|
||||||
|
|||||||
@@ -13,8 +13,11 @@
|
|||||||
//! So an interactive Plasma session does NOT hand it to a bare client — the host packages ship
|
//! So an interactive Plasma session does NOT hand it to a bare client — the host packages ship
|
||||||
//! `io.unom.Punktfunk.Host.desktop` (`Exec=/usr/bin/punktfunk-host`,
|
//! `io.unom.Punktfunk.Host.desktop` (`Exec=/usr/bin/punktfunk-host`,
|
||||||
//! `X-KDE-Wayland-Interfaces=zkde_screencast_unstable_v1,…`) so it is present before the host first
|
//! `X-KDE-Wayland-Interfaces=zkde_screencast_unstable_v1,…`) so it is present before the host first
|
||||||
//! connects. The headless test path instead exposes it to bare clients via
|
//! connects. That identification is also why **the host binary must carry no file capability**: a
|
||||||
//! `KWIN_WAYLAND_NO_PERMISSION_CHECKS=1`. The compositor backend must implement
|
//! process holding capabilities KWin lacks is one the kernel will not let KWin resolve
|
||||||
|
//! `/proc/<pid>/exe` for, so it can never be matched to a `.desktop` no matter how correctly the
|
||||||
|
//! file is installed (see [`capability_denial_hint`]). The headless test path instead exposes it to
|
||||||
|
//! bare clients via `KWIN_WAYLAND_NO_PERMISSION_CHECKS=1`. The compositor backend must implement
|
||||||
//! `createVirtualOutput`: the **DRM backend** (any version) or the **VirtualBackend since KWin
|
//! `createVirtualOutput`: the **DRM backend** (any version) or the **VirtualBackend since KWin
|
||||||
//! 6.5.6** (`kwin_wayland --virtual`); on `--virtual` < 6.5.6 the request fails with
|
//! 6.5.6** (`kwin_wayland --virtual`); on `--virtual` < 6.5.6 the request fails with
|
||||||
//! "Could not find output". We talk raw Wayland on `$WAYLAND_DISPLAY`, so the host must run inside
|
//! "Could not find output". We talk raw Wayland on `$WAYLAND_DISPLAY`, so the host must run inside
|
||||||
@@ -1071,6 +1074,74 @@ impl Drop for StopOnDrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extra sentence appended to every "KWin never advertised the screencast global" error when this
|
||||||
|
/// process carries capabilities — the one cause that is completely invisible from the Wayland side.
|
||||||
|
///
|
||||||
|
/// KWin authorizes a restricted interface by resolving the *client's* `/proc/<pid>/exe` and
|
||||||
|
/// matching it against an installed `.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 has no capabilities at all. So a host binary carrying any
|
||||||
|
/// file capability is simply unidentifiable: `executablePath()` comes back empty, no `.desktop` can
|
||||||
|
/// match, and the global is never advertised — indistinguishable, from here, from a missing
|
||||||
|
/// `.desktop`. Neither half of the obvious workaround helps: `prctl(PR_SET_DUMPABLE, 1)` leaves the
|
||||||
|
/// permitted-set check failing, and moving the grant to systemd `AmbientCapabilities=` lands the
|
||||||
|
/// capability in the same permitted set. Only an uncapped binary is identifiable.
|
||||||
|
///
|
||||||
|
/// This is not hypothetical: 0.26.0-1 setcap'd `cap_sys_nice` on the host for the GPU-priority
|
||||||
|
/// lever and took out desktop streaming on every KDE box until the capability was removed again.
|
||||||
|
fn capability_denial_hint() -> String {
|
||||||
|
let permitted = std::fs::read_to_string("/proc/self/status")
|
||||||
|
.ok()
|
||||||
|
.and_then(|status| permitted_caps_from_status(&status));
|
||||||
|
match permitted {
|
||||||
|
Some(caps) if caps != 0 => format!(
|
||||||
|
" — NOTE: this process carries capabilities (CapPrm={caps:#018x}), which is enough on \
|
||||||
|
its own to cause this: the kernel then refuses KWin the /proc/<pid>/exe read it \
|
||||||
|
identifies clients by, so no .desktop can match however correctly it is installed. \
|
||||||
|
Clear them with `sudo setcap -r /usr/bin/punktfunk-host` and restart the host"
|
||||||
|
),
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The permitted-capability mask out of a `/proc/<pid>/status` body, or `None` if the field is
|
||||||
|
/// absent/unparseable. The kernel prints it as a tab-separated 16-digit hex word with no `0x`
|
||||||
|
/// (`CapPrm:\t0000000000800000` = CAP_SYS_NICE), which is what the split-and-radix-16 parse below
|
||||||
|
/// expects — split out from [`capability_denial_hint`] purely so that shape is testable without a
|
||||||
|
/// capability-carrying process to point at.
|
||||||
|
fn permitted_caps_from_status(status: &str) -> Option<u64> {
|
||||||
|
let field = status.lines().find(|l| l.starts_with("CapPrm:"))?;
|
||||||
|
u64::from_str_radix(field.split_whitespace().nth(1)?, 16).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod capability_hint_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Verbatim from a `cap_sys_nice=ep` process on CachyOS — the case that broke 0.26.0-1.
|
||||||
|
const CAPPED: &str = "Name:\tpunktfunk-host\nUid:\t1000\t1000\t1000\t1000\nCapPrm:\t0000000000800000\nCapEff:\t0000000000800000\n";
|
||||||
|
/// ...and from the same binary with no capability, where the hint must stay silent.
|
||||||
|
const CLEAN: &str = "Name:\tpunktfunk-host\nUid:\t1000\t1000\t1000\t1000\nCapPrm:\t0000000000000000\nCapEff:\t0000000000000000\n";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_the_kernels_permitted_mask() {
|
||||||
|
assert_eq!(permitted_caps_from_status(CAPPED), Some(0x0080_0000));
|
||||||
|
assert_eq!(permitted_caps_from_status(CLEAN), Some(0));
|
||||||
|
// CapPrm is not guaranteed present (older/again-different kernels): stay quiet, never panic.
|
||||||
|
assert_eq!(permitted_caps_from_status("Name:\tx\n"), None);
|
||||||
|
assert_eq!(permitted_caps_from_status("CapPrm:\tzzzz\n"), None);
|
||||||
|
assert_eq!(permitted_caps_from_status("CapPrm:\n"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A capability-free host must not append the hint — the message it decorates is also printed
|
||||||
|
/// on genuinely missing `.desktop` files, and a spurious "you have capabilities" line would
|
||||||
|
/// send the reader chasing a setcap that was never there. The test process has no capabilities.
|
||||||
|
#[test]
|
||||||
|
fn silent_without_capabilities() {
|
||||||
|
assert_eq!(capability_denial_hint(), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Readiness probe: connect to the KWin Wayland socket, roundtrip the registry, and confirm
|
/// Readiness probe: connect to the KWin Wayland socket, roundtrip the registry, and confirm
|
||||||
/// the privileged `zkde_screencast` global is actually advertised. This is exactly what
|
/// the privileged `zkde_screencast` global is actually advertised. This is exactly what
|
||||||
/// [`run`] needs before it can create a virtual output, so a session-bringup script can poll
|
/// [`run`] needs before it can create a virtual output, so a session-bringup script can poll
|
||||||
@@ -1090,7 +1161,8 @@ pub fn probe() -> Result<()> {
|
|||||||
it on the host's .desktop X-KDE-Wayland-Interfaces (install \
|
it on the host's .desktop X-KDE-Wayland-Interfaces (install \
|
||||||
io.unom.Punktfunk.Host.desktop with Exec=/usr/bin/punktfunk-host, then re-login so KWin \
|
io.unom.Punktfunk.Host.desktop with Exec=/usr/bin/punktfunk-host, then re-login so KWin \
|
||||||
re-reads it — the grant is cached per-exe on first connect), or set \
|
re-reads it — the grant is cached per-exe on first connect), or set \
|
||||||
KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 for the headless test; needs KWin ≥ 6.5.6"
|
KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 for the headless test; needs KWin ≥ 6.5.6{}",
|
||||||
|
capability_denial_hint()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1134,7 +1206,9 @@ fn run_existing(
|
|||||||
anyhow!(
|
anyhow!(
|
||||||
"KWin does not expose zkde_screencast_unstable_v1 to this client — install the host's \
|
"KWin does not expose zkde_screencast_unstable_v1 to this client — install the host's \
|
||||||
.desktop (io.unom.Punktfunk.Host.desktop, X-KDE-Wayland-Interfaces) and re-login so \
|
.desktop (io.unom.Punktfunk.Host.desktop, X-KDE-Wayland-Interfaces) and re-login so \
|
||||||
KWin authorizes it, or run KWin with KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 (headless test)"
|
KWin authorizes it, or run KWin with KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 (headless \
|
||||||
|
test){}",
|
||||||
|
capability_denial_hint()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -1223,7 +1297,9 @@ fn run(
|
|||||||
anyhow!(
|
anyhow!(
|
||||||
"KWin does not expose zkde_screencast_unstable_v1 to this client — install the host's \
|
"KWin does not expose zkde_screencast_unstable_v1 to this client — install the host's \
|
||||||
.desktop (io.unom.Punktfunk.Host.desktop, X-KDE-Wayland-Interfaces) and re-login so \
|
.desktop (io.unom.Punktfunk.Host.desktop, X-KDE-Wayland-Interfaces) and re-login so \
|
||||||
KWin authorizes it, or run KWin with KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 (headless test)"
|
KWin authorizes it, or run KWin with KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 (headless \
|
||||||
|
test){}",
|
||||||
|
capability_denial_hint()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
@@ -195,6 +195,16 @@ bash /usr/share/punktfunk/bazzite/kde-desktop-setup.sh
|
|||||||
|
|
||||||
Gaming Mode needs none of this — it auto-attaches.
|
Gaming Mode needs none of this — it auto-attaches.
|
||||||
|
|
||||||
|
> **Version 0.26.0-1 cannot stream the Desktop, and nothing on this page fixes it.** Every session
|
||||||
|
> ends with `KWin does not expose zkde_screencast_unstable_v1 to this client`. That image gave the
|
||||||
|
> host binary the `CAP_SYS_NICE` capability, and Linux will not let KWin read the `/proc/<pid>/exe`
|
||||||
|
> it uses to work out which program is asking — so the grant above can never match, however
|
||||||
|
> correctly it is installed. Reinstalling does not help: it looks like a missing `.desktop` file and
|
||||||
|
> is not one. The image's `/usr` is read-only, so it cannot be repaired on the box — take the next
|
||||||
|
> image with `sudo punktfunk-sysext update`, then
|
||||||
|
> `systemctl --user restart punktfunk-host`. Gaming Mode is unaffected; gamescope has no such
|
||||||
|
> identity check.
|
||||||
|
|
||||||
## Run as an always-on host
|
## Run as an always-on host
|
||||||
|
|
||||||
Bazzite hosts are typically headless. Enable the host service and linger so it starts at boot — see
|
Bazzite hosts are typically headless. Enable the host service and linger so it starts at boot — see
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ notes for context.
|
|||||||
| `PUNKTFUNK_NVENC_SPLIT_ARBITRATE` | `1` | Opt-in: let the host change its split-encode decision **live**, mid-session, as the pixel rate moves, instead of only choosing once at session start. Currently wired on the Linux direct-NVENC path. Only interesting alongside `PUNKTFUNK_SPLIT_ENCODE=auto` at very high pixel rates. |
|
| `PUNKTFUNK_NVENC_SPLIT_ARBITRATE` | `1` | Opt-in: let the host change its split-encode decision **live**, mid-session, as the pixel rate moves, instead of only choosing once at session start. Currently wired on the Linux direct-NVENC path. Only interesting alongside `PUNKTFUNK_SPLIT_ENCODE=auto` at very high pixel rates. |
|
||||||
| `PUNKTFUNK_GPU_PRIORITY_CLASS` | `off` · `normal` · `high` · `realtime` · `auto` | **(Windows)** GPU scheduling priority for capture/encode under a GPU-saturating game. Default `auto` (starts `high`, upgrades to `realtime` when it's safe — e.g. HAGS off); `high` pins the static pre-gate behaviour; `realtime` is the strongest lever but can freeze NVENC on some setups. |
|
| `PUNKTFUNK_GPU_PRIORITY_CLASS` | `off` · `normal` · `high` · `realtime` · `auto` | **(Windows)** GPU scheduling priority for capture/encode under a GPU-saturating game. Default `auto` (starts `high`, upgrades to `realtime` when it's safe — e.g. HAGS off); `high` pins the static pre-gate behaviour; `realtime` is the strongest lever but can freeze NVENC on some setups. |
|
||||||
| `PUNKTFUNK_IDD_DEPTH` | `N` (default `2`) | **(Windows)** IDD-push pipeline depth. `1` cuts latency once GPU priority is raised; higher smooths a contended GPU. |
|
| `PUNKTFUNK_IDD_DEPTH` | `N` (default `2`) | **(Windows)** IDD-push pipeline depth. `1` cuts latency once GPU priority is raised; higher smooths a contended GPU. |
|
||||||
| `PYROWAVE_QUEUE_PRIORITY` | `realtime` *(default)* · `high` · `off` | [PyroWave](/docs/pyrowave) sessions only. PyroWave encodes on the same GPU shader cores a game uses, so a demanding game can starve it and the frame rate drops. This asks the driver to schedule the encode ahead of the game. `realtime` tries the strongest class and falls back to `high`; `high` asks only for the middle one; `off` disables the request. A driver that refuses simply encodes at normal priority — it can never stop a session starting. The packages grant the host the `CAP_SYS_NICE` capability this needs; on a host built or installed by hand it will be refused, and the host says so once at session start. Set `off` if you see the desktop stutter while streaming. |
|
| `PYROWAVE_QUEUE_PRIORITY` | `realtime` *(default)* · `high` · `off` | [PyroWave](/docs/pyrowave) sessions only. PyroWave encodes on the same GPU shader cores a game uses, so a demanding game can starve it and the frame rate drops. This asks the driver to schedule the encode ahead of the game. `realtime` tries the strongest class and falls back to `high`; `high` asks only for the middle one; `off` disables the request. A driver that refuses simply encodes at normal priority — it can never stop a session starting. This needs the `CAP_SYS_NICE` capability, which the packages deliberately do **not** grant: a host holding a capability cannot be identified by KWin and loses desktop streaming entirely (see [Running as a service](/docs/running-as-a-service#gpu-scheduling-priority)). The request is therefore refused on a stock install and the host says so once at session start. Set `off` if you see the desktop stutter while streaming. |
|
||||||
|
|
||||||
## Diagnostics
|
## Diagnostics
|
||||||
|
|
||||||
|
|||||||
@@ -207,33 +207,41 @@ a Windows host, run `punktfunk-host service status` from an elevated prompt on t
|
|||||||
|
|
||||||
## GPU scheduling priority
|
## GPU scheduling priority
|
||||||
|
|
||||||
The Linux packages give the host binary one Linux capability, `CAP_SYS_NICE`, and it is worth
|
The host binary carries **no Linux capability**, and on a KDE desktop it must not.
|
||||||
knowing why it is there and how to take it away.
|
|
||||||
|
|
||||||
The [PyroWave](/docs/pyrowave) codec encodes on the same GPU shader cores your game is using, so a
|
The [PyroWave](/docs/pyrowave) codec encodes on the same GPU shader cores your game is using, so a
|
||||||
demanding game can crowd it out and the stream's frame rate drops with it. The fix is to ask the
|
demanding game can crowd it out and the stream's frame rate drops with it. The fix is to ask the
|
||||||
driver to schedule the encode ahead of the game, and every driver we tested gates that request on
|
driver to schedule the encode ahead of the game, and every driver we tested gates that request on
|
||||||
this capability: without it the request is simply refused and nothing changes. The other codecs use
|
`CAP_SYS_NICE`. Version 0.26.0-1 granted it for that reason — and it broke desktop streaming on
|
||||||
a separate video engine on the GPU and are unaffected either way.
|
every KDE box, so 0.26.0-2 takes it away again. The other codecs use a separate video engine on the
|
||||||
|
GPU and were never affected.
|
||||||
|
|
||||||
`CAP_SYS_NICE` lets a process raise its own scheduling priority. It grants no access to files,
|
The two cannot coexist. To hand the host its virtual display, KWin first has to work out *which*
|
||||||
the network or other users' processes, and it is **not** the same as running as root — the host
|
program is asking, which it does by reading the connecting process's `/proc/<pid>/exe` and matching
|
||||||
still runs as you, under your user session.
|
it against the `.desktop` file the packages install. Linux refuses that read for any process holding
|
||||||
|
a capability the reader does not also hold — and KWin holds none. So a host with `CAP_SYS_NICE` is a
|
||||||
|
host KWin cannot identify, and every session fails with:
|
||||||
|
|
||||||
To check, or to take it away:
|
```
|
||||||
|
KWin virtual output failed: KWin does not expose zkde_screencast_unstable_v1 to this client
|
||||||
```sh
|
|
||||||
getcap /usr/bin/punktfunk-host # shows cap_sys_nice=ep when granted
|
|
||||||
sudo setcap -r /usr/bin/punktfunk-host # remove it; streaming still works
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Removing it costs you nothing unless you stream PyroWave, and you can also just set
|
which looks exactly like a missing `.desktop` file and cannot be fixed by reinstalling. Moving the
|
||||||
`PYROWAVE_QUEUE_PRIORITY=off` to stop the host asking. Note that a package update replaces the
|
grant into the systemd unit does not help either — same capability, same refused read.
|
||||||
binary and re-applies the capability.
|
|
||||||
|
|
||||||
Two side effects, if you are debugging the host: a binary carrying a capability is treated as
|
If you are on 0.26.0-1, update. On the Bazzite image the `/usr` is read-only, so the only repair is
|
||||||
security-sensitive by the dynamic loader, so `LD_LIBRARY_PATH` and `LD_PRELOAD` are ignored for it,
|
the next image (`sudo punktfunk-sysext update`). Elsewhere you can clear it by hand:
|
||||||
and it does not write core dumps by default.
|
|
||||||
|
```sh
|
||||||
|
getcap /usr/bin/punktfunk-host # prints nothing when correct
|
||||||
|
sudo setcap -r /usr/bin/punktfunk-host # clear it, then restart the host
|
||||||
|
```
|
||||||
|
|
||||||
|
Losing the capability costs frame pacing under a GPU-bound game and nothing else — the host asks for
|
||||||
|
the elevated priority, is refused, and encodes at the normal one. `PYROWAVE_QUEUE_PRIORITY=off`
|
||||||
|
stops it asking at all. If you stream only with gamescope (Steam Gaming Mode) you can grant the
|
||||||
|
capability yourself and keep the pacing, at the cost of desktop streaming; gamescope has no such
|
||||||
|
identity check.
|
||||||
|
|
||||||
## Stopping and removing
|
## Stopping and removing
|
||||||
|
|
||||||
|
|||||||
@@ -173,8 +173,10 @@ systemctl --user enable --now punktfunk-host # the user unit is now under /u
|
|||||||
```
|
```
|
||||||
The udev rule, sysctl, and systemd **user** unit all live under `/usr/lib`, so the merged sysext
|
The udev rule, sysctl, and systemd **user** unit all live under `/usr/lib`, so the merged sysext
|
||||||
exposes them. `systemd-sysext refresh` re-merges after a reboot. (One HDR nuance of the sysext
|
exposes them. `systemd-sysext refresh` re-merges after a reboot. (One HDR nuance of the sysext
|
||||||
path: file capabilities don't survive it, so gamescope runs without `CAP_SYS_NICE` — everything
|
path: the image ships gamescope without `CAP_SYS_NICE`, so its frame pacing is marginally worse —
|
||||||
works, frame pacing is marginally worse than the pacman install, whose `.install` sets the cap.)
|
everything works. Note the host binary carries no capability on *either* path, deliberately: one
|
||||||
|
would make the host unidentifiable to KWin and break desktop streaming, see
|
||||||
|
[Running as a service](https://punktfunk.io/docs/running-as-a-service#gpu-scheduling-priority).)
|
||||||
|
|
||||||
## Steam Deck — the client (what the Decky plugin launches)
|
## Steam Deck — the client (what the Decky plugin launches)
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,17 @@
|
|||||||
# instead of 8-bit SDR (the host prefers that name on PATH and attempts HDR by default). Mirrors
|
# 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
|
# 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 —
|
# 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: file
|
# the host then stays SDR on that backend, by design.
|
||||||
# capabilities don't survive this squashfs path — gamescope runs without it, pacing slightly
|
#
|
||||||
# worse, same as the Bazzite sysext.)
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
GAMESCOPE=""
|
GAMESCOPE=""
|
||||||
|
|||||||
@@ -12,33 +12,48 @@ _ensure_punktfunk_group() {
|
|||||||
getent group punktfunk >/dev/null 2>&1 || groupadd --system punktfunk 2>/dev/null || true
|
getent group punktfunk >/dev/null 2>&1 || groupadd --system punktfunk 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# CAP_SYS_NICE on the host binary — the GPU-scheduling grant.
|
# NO capability on the host binary — and an active removal of the one 0.26.0-1 granted.
|
||||||
#
|
#
|
||||||
# WHY: PyroWave encodes on the GPU's shader cores, so a GPU-bound game starves it (measured: the
|
# 0.26.0-1 ran `setcap cap_sys_nice=ep` here, to let the encoder open an elevated global-priority
|
||||||
# encode dispatch goes from ~2 ms to 15-18 ms at 95 % game load). The fix is an elevated
|
# Vulkan queue (PyroWave shares the GPU's shader cores with the game; measured 2026-08-08 on an
|
||||||
# global-priority Vulkan queue, which the driver gates on CAP_SYS_NICE — 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
|
||||||
# RTX 5070 Ti: WITHOUT the capability every priority class is refused, WITH it the encoder is
|
# BROKE DESKTOP STREAMING ON EVERY KDE BOX, and it cannot be made to work — the two are mutually
|
||||||
# granted REALTIME on the first attempt. RADV is the same. Without this line the knob exists and
|
# exclusive at the kernel level:
|
||||||
# does nothing. Same capability, same mechanism, as our gamescope package sets on its own binary.
|
|
||||||
#
|
#
|
||||||
# NARROW: CAP_SYS_NICE only permits raising scheduling priority (nice/ioprio/affinity/RT class). It
|
# KWin hands out its restricted Wayland protocols (zkde_screencast_unstable_v1, which mints our
|
||||||
# grants no filesystem, network or user-switching privilege, and it is NOT setuid.
|
# 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.
|
||||||
#
|
#
|
||||||
# TWO CONSEQUENCES worth knowing before you debug something odd on this host:
|
# Verified on CachyOS (kernel 7.1.6), same-uid reader, cap_sys_nice=ep on the target:
|
||||||
# * a file capability makes the process AT_SECURE, so the dynamic loader IGNORES LD_LIBRARY_PATH
|
# no capability .............................. readlink /proc/<pid>/exe OK
|
||||||
# and LD_PRELOAD for it. A library-path shim that used to work will silently stop.
|
# capability ................................. EPERM
|
||||||
# * core dumps are suppressed for capability-carrying binaries by default (fs.suid_dumpable).
|
# 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
|
||||||
#
|
#
|
||||||
# Never fails the install: a box without libcap, or a filesystem that cannot store capabilities
|
# The third row also rules out the obvious "move it to the systemd unit": AmbientCapabilities= puts
|
||||||
# (some overlay/NFS setups), just runs at default priority exactly as before.
|
# CAP_SYS_NICE in exactly the same permitted set and fails identically. Nothing short of not having
|
||||||
_grant_sched_capability() {
|
# the capability restores identification, so the host does not get one. The encoder already walks
|
||||||
setcap 'cap_sys_nice=ep' usr/bin/punktfunk-host 2>/dev/null || true
|
# 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() {
|
post_install() {
|
||||||
_ensure_update_group
|
_ensure_update_group
|
||||||
_ensure_punktfunk_group
|
_ensure_punktfunk_group
|
||||||
_grant_sched_capability
|
_revoke_sched_capability
|
||||||
udevadm control --reload-rules 2>/dev/null || true
|
udevadm control --reload-rules 2>/dev/null || true
|
||||||
udevadm trigger --subsystem-match=misc 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).
|
# Apply the UDP socket-buffer tuning now (also auto-applied at boot by systemd-sysctl).
|
||||||
@@ -97,8 +112,8 @@ post_upgrade() {
|
|||||||
# root-only, and the virtual Steam Deck pad silently unable to attach. groupadd is idempotent, so
|
# 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.
|
# this is a no-op on boxes that installed fresh.
|
||||||
_ensure_punktfunk_group
|
_ensure_punktfunk_group
|
||||||
# A replaced binary is a NEW inode — file capabilities do not survive the upgrade, so re-grant.
|
# Strip the cap_sys_nice 0.26.0-1 granted: it makes the host unidentifiable to KWin (see above).
|
||||||
_grant_sched_capability
|
_revoke_sched_capability
|
||||||
udevadm control --reload-rules 2>/dev/null || true
|
udevadm control --reload-rules 2>/dev/null || true
|
||||||
sysctl -p /usr/lib/sysctl.d/99-punktfunk-net.conf >/dev/null 2>&1 || true
|
sysctl -p /usr/lib/sysctl.d/99-punktfunk-net.conf >/dev/null 2>&1 || true
|
||||||
_warn_stale_firewall_ports
|
_warn_stale_firewall_ports
|
||||||
|
|||||||
@@ -421,11 +421,23 @@ bash /usr/share/punktfunk/bazzite/kde-desktop-setup.sh
|
|||||||
# then log out + back into the KDE Desktop session once (or reboot) so KWin restarts with the flag
|
# then log out + back into the KDE Desktop session once (or reboot) so KWin restarts with the flag
|
||||||
```
|
```
|
||||||
|
|
||||||
That writes `~/.config/environment.d/10-punktfunk-kwin.conf`
|
That seeds the `kde-authorized` RemoteDesktop grant into `~/.local/share/flatpak/db/` — the input
|
||||||
(`KWIN_WAYLAND_NO_PERMISSION_CHECKS=1`) and seeds the `kde-authorized` RemoteDesktop grant into
|
half. The **video** half needs no session-wide override: the image ships
|
||||||
`~/.local/share/flatpak/db/`. Gaming Mode is unaffected. To connect from Desktop Mode, switch to it
|
`io.unom.Punktfunk.Host.desktop`, whose `X-KDE-Wayland-Interfaces` grants the host KWin's
|
||||||
(Steam → Power → Switch to Desktop), then connect the client; switching **mid-stream** requires a
|
`zkde_screencast` protocol on a normal Plasma login (least-privilege — only this binary, only that
|
||||||
reconnect (the host resolves the backend per connect).
|
interface). Older versions of the script wrote a session-wide
|
||||||
|
`KWIN_WAYLAND_NO_PERMISSION_CHECKS=1` into `~/.config/environment.d/10-punktfunk-kwin.conf`; it now
|
||||||
|
*removes* that file as an over-broad leftover. Gaming Mode is unaffected. To connect from Desktop
|
||||||
|
Mode, switch to it (Steam → Power → Switch to Desktop), then connect the client; switching
|
||||||
|
**mid-stream** requires a reconnect (the host resolves the backend per connect).
|
||||||
|
|
||||||
|
> **On 0.26.0-1 specifically, Desktop mode is broken and no amount of this setup fixes it.** That
|
||||||
|
> image shipped `cap_sys_nice=ep` on `/usr/bin/punktfunk-host`, and a capability-carrying process is
|
||||||
|
> one KWin cannot identify (it resolves `/proc/<pid>/exe` to match the `.desktop`, and the kernel
|
||||||
|
> refuses that read), so the session dies with `KWin does not expose zkde_screencast_unstable_v1 to
|
||||||
|
> this client`. A merged sysext's `/usr` is read-only, so it cannot be repaired in place — take the
|
||||||
|
> next image (`sudo punktfunk-sysext update`). `KWIN_WAYLAND_NO_PERMISSION_CHECKS=1` works around it
|
||||||
|
> meanwhile by disabling the check that needs the identification.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -130,28 +130,35 @@ SYSEXT_VERSION_ID=$PF_VR
|
|||||||
EXTENSION_RELOAD_MANAGER=1
|
EXTENSION_RELOAD_MANAGER=1
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# CAP_SYS_NICE on the host binary — the GPU-scheduling grant. PyroWave encodes on the GPU shader
|
# NO CAP_SYS_NICE in the image — and an assertion that none crept back in.
|
||||||
# cores a game saturates, and the driver gates the elevated global-priority Vulkan queue that fixes
|
|
||||||
# it on this capability (measured 2026-08-08 on an RTX 5070 Ti: refused without it, granted REALTIME
|
|
||||||
# with it; RADV the same). Narrow — scheduling priority only, no filesystem/network privilege, not
|
|
||||||
# setuid.
|
|
||||||
#
|
#
|
||||||
# It has to be applied HERE, not in the merge hook: a merged sysext's /usr is a read-only squashfs,
|
# 0.26.0-1 setcap'd the staged binary here for the GPU-priority lever. mksquashfs records
|
||||||
# so nothing can setcap it afterwards. And it cannot ride in from the RPM either — the spec declares
|
# security.capability, so the capability really did ship: verified by mounting the published
|
||||||
# it with %caps, but rpm stores capabilities in its own header and `rpm2cpio | cpio` carries only
|
# punktfunk-0.26.0-1-x86-64.raw, where `getcap usr/bin/punktfunk-host` reports `cap_sys_nice=ep`.
|
||||||
# the payload, so the staged file arrives with no capability at all. mksquashfs DOES record
|
# That broke desktop streaming on every Bazzite KDE box, field-reported as
|
||||||
# security.capability (only security.selinux is excluded below), so a setcap on the staging tree is
|
# "KWin does not expose zkde_screencast_unstable_v1 to this client".
|
||||||
# what ends up in the image.
|
|
||||||
#
|
#
|
||||||
# Needs CAP_SETFCAP, i.e. root (or fakeroot) — a plain-user CI build cannot do it. That is not fatal:
|
# KWin advertises its restricted protocols (zkde_screencast_unstable_v1 for the virtual output,
|
||||||
# the image just ships as it does today and the encode runs at default GPU priority, so warn and
|
# org_kde_kwin_fake_input for input) only to a client it can IDENTIFY, by resolving that client's
|
||||||
# carry on rather than fail a release build over a performance lever.
|
# /proc/<pid>/exe and matching it against an installed .desktop's Exec= — the image ships
|
||||||
if [ -f "$STAGE/usr/bin/punktfunk-host" ]; then
|
# usr/share/applications/io.unom.Punktfunk.Host.desktop for exactly that. The kernel refuses that
|
||||||
if setcap 'cap_sys_nice=ep' "$STAGE/usr/bin/punktfunk-host" 2>/dev/null; then
|
# readlink to any reader whose effective set is not a superset of the target's PERMITTED set
|
||||||
echo "granted CAP_SYS_NICE to usr/bin/punktfunk-host (GPU-priority lever active)"
|
# (cap_ptrace_access_check), and KWin holds no capabilities. So a capability in this image makes the
|
||||||
else
|
# host unidentifiable and every Desktop-mode session dies. Full matrix, including why neither
|
||||||
echo "WARNING: could not setcap CAP_SYS_NICE (need root/CAP_SETFCAP) — the image will ship" >&2
|
# prctl(PR_SET_DUMPABLE, 1) nor systemd AmbientCapabilities= rescues it, in
|
||||||
echo " without it and PyroWave will encode at default GPU priority." >&2
|
# packaging/arch/punktfunk-host.install.
|
||||||
|
#
|
||||||
|
# A merged sysext's /usr is a read-only squashfs, so this cannot be repaired on the box — the image
|
||||||
|
# is the only place it can be got right. Assert it rather than trust it: the RPM payload arrives via
|
||||||
|
# `rpm2cpio | cpio`, which carries no capabilities today, but the spec is one `%caps()` away from
|
||||||
|
# changing that and this build would silently bake it in.
|
||||||
|
if [ -f "$STAGE/usr/bin/punktfunk-host" ] && command -v getcap >/dev/null 2>&1; then
|
||||||
|
staged_caps="$(getcap "$STAGE/usr/bin/punktfunk-host" 2>/dev/null || true)"
|
||||||
|
if [ -n "$staged_caps" ]; then
|
||||||
|
echo "ERROR: staged usr/bin/punktfunk-host carries capabilities: $staged_caps" >&2
|
||||||
|
echo " A capability makes the host unidentifiable to KWin and breaks every Desktop-mode" >&2
|
||||||
|
echo " session on a merged image, which cannot be repaired on the box (read-only /usr)." >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -294,16 +294,23 @@ if [ "$1" = "configure" ]; then
|
|||||||
# primitive that must not ride on the group users are told to join for gamepads
|
# primitive that must not ride on the group users are told to join for gamepads
|
||||||
# (security-review 2026-08-05 M-4).
|
# (security-review 2026-08-05 M-4).
|
||||||
getent group punktfunk >/dev/null 2>&1 || addgroup --system punktfunk 2>/dev/null || true
|
getent group punktfunk >/dev/null 2>&1 || addgroup --system punktfunk 2>/dev/null || true
|
||||||
# CAP_SYS_NICE — the GPU-scheduling grant. PyroWave encodes on the shader cores a game
|
# NO capability on the host binary — and an active removal of the one 0.26.0-1 granted here.
|
||||||
# saturates, and the driver gates the elevated global-priority Vulkan queue that fixes it on
|
#
|
||||||
# this capability: measured 2026-08-08 on an RTX 5070 Ti, WITHOUT it every priority class is
|
# 0.26.0-1 ran `setcap cap_sys_nice=ep` at this point for the GPU-priority lever, and that broke
|
||||||
# refused and WITH it the encoder is granted REALTIME first try (RADV behaves the same).
|
# desktop streaming on every KDE box. KWin advertises its restricted protocols
|
||||||
# Without this line the knob exists and does nothing. Narrow: it permits raising scheduling
|
# (zkde_screencast_unstable_v1 for the virtual output, org_kde_kwin_fake_input for input) only
|
||||||
# priority only — no filesystem, network or user-switching privilege, and no setuid. Note a
|
# to a client it can IDENTIFY, by resolving that client's /proc/<pid>/exe and matching it
|
||||||
# capability-carrying binary is AT_SECURE, so the loader ignores LD_LIBRARY_PATH/LD_PRELOAD for
|
# against an installed .desktop's Exec=. The kernel refuses that readlink to any reader whose
|
||||||
# it and core dumps are suppressed by default. Best-effort: a box without libcap, or a
|
# effective set is not a superset of the target's PERMITTED set (cap_ptrace_access_check), and
|
||||||
# filesystem that cannot store capabilities, just runs at default priority as before.
|
# KWin holds no capabilities — so a capability here makes the host unidentifiable and the
|
||||||
setcap 'cap_sys_nice=ep' /usr/bin/punktfunk-host 2>/dev/null || true
|
# session dies with "KWin does not expose zkde_screencast_unstable_v1 to this client". Full
|
||||||
|
# matrix (and why PR_SET_DUMPABLE and AmbientCapabilities= both fail to rescue it) in
|
||||||
|
# packaging/arch/punktfunk-host.install.
|
||||||
|
#
|
||||||
|
# Costs pacing only: pf-zerocopy walks REALTIME -> HIGH -> default when a class is refused.
|
||||||
|
# postinst runs on upgrade too, so this heals boxes that installed 0.26.0-1. `setcap -r` exits
|
||||||
|
# non-zero on a file that has no capability, hence the redirect and `|| true`.
|
||||||
|
setcap -r /usr/bin/punktfunk-host 2>/dev/null || true
|
||||||
# Pick up the /dev/uinput rule without a reboot (best-effort, no-op in containers).
|
# Pick up the /dev/uinput rule without a reboot (best-effort, no-op in containers).
|
||||||
udevadm control --reload-rules 2>/dev/null || true
|
udevadm control --reload-rules 2>/dev/null || true
|
||||||
udevadm trigger --subsystem-match=misc 2>/dev/null || true
|
udevadm trigger --subsystem-match=misc 2>/dev/null || true
|
||||||
|
|||||||
@@ -356,25 +356,25 @@ in
|
|||||||
allowedUDPPorts = nativeUDP ++ optionals cfg.host.gamestream gamestreamUDP;
|
allowedUDPPorts = nativeUDP ++ optionals cfg.host.gamestream gamestreamUDP;
|
||||||
};
|
};
|
||||||
|
|
||||||
# CAP_SYS_NICE — the GPU-scheduling grant. PyroWave encodes on the GPU shader cores a game
|
# NO CAP_SYS_NICE wrapper here — deliberately. 0.26.0-1 gave the host a
|
||||||
# saturates; the elevated global-priority Vulkan queue that fixes it is gated on this
|
# `security.wrappers.punktfunk-host` carrying `cap_sys_nice=ep` for the GPU-priority lever,
|
||||||
# capability (measured 2026-08-08, RTX 5070 Ti: without it EVERY priority class is refused,
|
# and that broke desktop streaming on every KDE box.
|
||||||
# with it the encoder gets REALTIME on the first attempt; RADV behaves the same).
|
|
||||||
#
|
#
|
||||||
# NixOS cannot `setcap` a store path — it is read-only and shared — so this goes through
|
# KWin advertises its restricted Wayland protocols (zkde_screencast_unstable_v1 for the
|
||||||
# `security.wrappers`, which builds a small setcap'd wrapper in /run/wrappers/bin. The unit's
|
# virtual output, org_kde_kwin_fake_input for input) only to a client it can IDENTIFY, by
|
||||||
# ExecStart points at the wrapper below; everything else about the host is unchanged.
|
# resolving that client's /proc/<pid>/exe and matching it against an installed .desktop's
|
||||||
|
# Exec= (packages.nix substitutes ours to the store path). 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.
|
||||||
#
|
#
|
||||||
# Narrow: CAP_SYS_NICE permits raising scheduling priority only — no filesystem, network or
|
# A NixOS wrapper does not dodge this. It raises the capability into its AMBIENT set before
|
||||||
# user-switching privilege, and the wrapper is capability-based, NOT setuid. Two side effects
|
# exec'ing the store binary, precisely so the capability survives — which lands CAP_SYS_NICE
|
||||||
# to know: the wrapped binary is AT_SECURE (the loader ignores LD_LIBRARY_PATH/LD_PRELOAD for
|
# in the exec'd process's permitted set and fails the readlink identically. Measured: an
|
||||||
# it) and core dumps are suppressed by default.
|
# ambient-only grant (dumpable=1, CapPrm set) is refused exactly like a file capability. See
|
||||||
security.wrappers.punktfunk-host = {
|
# packaging/arch/punktfunk-host.install for the full matrix.
|
||||||
source = "${cfg.host.package}/bin/punktfunk-host";
|
#
|
||||||
capabilities = "cap_sys_nice=ep";
|
# Costs pacing only: pf-zerocopy walks REALTIME -> HIGH -> default when a priority class is
|
||||||
owner = "root";
|
# refused, and pf-frame's thread nice is a best-effort no-op — 0.25.0's behaviour exactly.
|
||||||
group = "root";
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.user.services.punktfunk-host = {
|
systemd.user.services.punktfunk-host = {
|
||||||
description = "punktfunk GameStream + punktfunk/1 streaming host";
|
description = "punktfunk GameStream + punktfunk/1 streaming host";
|
||||||
@@ -394,12 +394,12 @@ in
|
|||||||
# PUNKTFUNK_GAMESCOPE_BIN so an operator's own override of that env still wins.
|
# PUNKTFUNK_GAMESCOPE_BIN so an operator's own override of that env still wins.
|
||||||
++ optional cfg.host.gamescopeHdr cfg.host.gamescopePackage;
|
++ optional cfg.host.gamescopeHdr cfg.host.gamescopePackage;
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
# Through the wrapper (see `security.wrappers.punktfunk-host` above), NOT the store path
|
# The store path DIRECTLY — not a capability wrapper. /proc/<pid>/exe then resolves to the
|
||||||
# directly — the store path carries no capability and the GPU-priority lever would be
|
# very path packages.nix substituted into io.unom.Punktfunk.Host.desktop's Exec=, which is
|
||||||
# inert. `config.security.wrapperDir` rather than a hard-coded /run/wrappers/bin so an
|
# what lets KWin identify the host and grant it the screencast/fake-input protocols (see
|
||||||
# operator who has moved it is still correct.
|
# the note above the firewall block).
|
||||||
ExecStart =
|
ExecStart =
|
||||||
"${config.security.wrapperDir}/punktfunk-host serve" + optionalString cfg.host.gamestream " --gamestream";
|
"${cfg.host.package}/bin/punktfunk-host serve" + optionalString cfg.host.gamestream " --gamestream";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = 2;
|
RestartSec = 2;
|
||||||
EnvironmentFile =
|
EnvironmentFile =
|
||||||
|
|||||||
@@ -477,15 +477,28 @@ install -Dm0644 scripts/punktfunk-scripting.service %{buildroot}%{_userunitdir}/
|
|||||||
%files
|
%files
|
||||||
%license LICENSE-MIT LICENSE-APACHE THIRD-PARTY-NOTICES.txt
|
%license LICENSE-MIT LICENSE-APACHE THIRD-PARTY-NOTICES.txt
|
||||||
%doc README.md packaging/README.md
|
%doc README.md packaging/README.md
|
||||||
# CAP_SYS_NICE — the GPU-scheduling grant, declared the RPM-native way so rpm applies it at
|
# NO %caps() on the host binary. 0.26.0-1 declared `%caps(cap_sys_nice=ep)` here for the
|
||||||
# install, restores it on upgrade, and VERIFIES it (a plain %post setcap does none of those).
|
# GPU-priority lever and that BROKE DESKTOP STREAMING ON EVERY KDE BOX — on Fedora and, via
|
||||||
# PyroWave encodes on the shader cores a game saturates; the elevated global-priority Vulkan queue
|
# rpm-ostree layering, on Bazzite, where it was field-reported as
|
||||||
# that fixes it is gated on this capability. Measured 2026-08-08 on an RTX 5070 Ti: without it
|
# "KWin does not expose zkde_screencast_unstable_v1 to this client".
|
||||||
# every priority class is refused, with it the encoder gets REALTIME first try (RADV the same).
|
#
|
||||||
# Narrow — scheduling priority only, no filesystem/network/user-switching privilege, not setuid.
|
# KWin hands out its restricted Wayland protocols (zkde_screencast_unstable_v1 for the virtual
|
||||||
# Consequences: the binary becomes AT_SECURE, so the loader ignores LD_LIBRARY_PATH/LD_PRELOAD for
|
# output, org_kde_kwin_fake_input for input) only to a client it can IDENTIFY, by resolving that
|
||||||
# it, and core dumps are suppressed by default.
|
# client's /proc/<pid>/exe and matching it against an installed .desktop's Exec= — ours is the
|
||||||
%caps(cap_sys_nice=ep) %{_bindir}/punktfunk-host
|
# io.unom.Punktfunk.Host.desktop installed below. 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 here makes the host unidentifiable: KWin's
|
||||||
|
# executablePath() is empty, no .desktop can match, and the globals are never advertised.
|
||||||
|
# Measured on kernel 7.1.6 — see packaging/arch/punktfunk-host.install for the full matrix, incl.
|
||||||
|
# why neither prctl(PR_SET_DUMPABLE, 1) nor systemd AmbientCapabilities= rescues it.
|
||||||
|
#
|
||||||
|
# The cost of not having it is pacing only: pf-zerocopy walks REALTIME -> HIGH -> default when a
|
||||||
|
# priority class is refused, and pf-frame's thread nice is a best-effort no-op. That is exactly
|
||||||
|
# how 0.25.0 behaved, which is the behaviour that worked.
|
||||||
|
#
|
||||||
|
# rpm applies file capabilities from package metadata, so a package built WITHOUT %caps() installs
|
||||||
|
# the binary with none and an upgrade from 0.26.0-1 clears it — no scriptlet needed.
|
||||||
|
%{_bindir}/punktfunk-host
|
||||||
%{_bindir}/punktfunk-tray
|
%{_bindir}/punktfunk-tray
|
||||||
%{_udevrulesdir}/60-punktfunk.rules
|
%{_udevrulesdir}/60-punktfunk.rules
|
||||||
%dir %{_libexecdir}/punktfunk
|
%dir %{_libexecdir}/punktfunk
|
||||||
|
|||||||
@@ -344,24 +344,23 @@ if [ "$SUDO_OK" = 1 ]; then
|
|||||||
warn "(everything else works; the pad arrives as a generic Xbox 360 controller). By hand:"
|
warn "(everything else works; the pad arrives as a generic Xbox 360 controller). By hand:"
|
||||||
warn " sudo groupadd --system punktfunk; sudo usermod -aG punktfunk $USER"
|
warn " sudo groupadd --system punktfunk; sudo usermod -aG punktfunk $USER"
|
||||||
fi
|
fi
|
||||||
# CAP_SYS_NICE on the host binary — the GPU-scheduling grant, and the Deck is the box that
|
# NO CAP_SYS_NICE on the host binary — and a removal of the one 0.26.0-1 granted here.
|
||||||
# needs it most: a Van Gogh APU shares one small GPU between the game and PyroWave's encode
|
|
||||||
# dispatch. The driver gates the elevated global-priority Vulkan queue on this capability
|
|
||||||
# (measured 2026-08-08 on an RTX 5070 Ti: refused without it, granted REALTIME with it; RADV
|
|
||||||
# behaves the same), so without this the knob exists and does nothing.
|
|
||||||
#
|
#
|
||||||
# The binary lives under $HOME, not /usr — so unlike the /etc drop-ins above this survives a
|
# 0.26.0-1 setcap'd this binary for the GPU-priority lever, which on a Van Gogh APU is a real
|
||||||
# SteamOS A/B update on its own and needs no atomic-keep entry. It DOES need re-applying after
|
# win. It also broke Desktop-mode streaming outright. Just above, this installer writes
|
||||||
# every rebuild, because a fresh binary is a new inode; re-running this installer does that.
|
# ~/.local/share/applications/io.unom.Punktfunk.Host.desktop with Exec=$BIN so KWin will grant
|
||||||
|
# the host its restricted protocols — and KWin makes that grant by resolving the client's
|
||||||
|
# /proc/<pid>/exe and matching it against that 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 capability silently voided
|
||||||
|
# the .desktop written six lines earlier, and every Desktop-mode session died with
|
||||||
|
# "KWin does not expose zkde_screencast_unstable_v1 to this client". Gaming Mode (gamescope) is
|
||||||
|
# unaffected — it has no such gate. Full matrix in packaging/arch/punktfunk-host.install.
|
||||||
#
|
#
|
||||||
# Narrow (scheduling priority only, no filesystem/network privilege, not setuid) and
|
# Costs pacing only: pf-zerocopy walks REALTIME -> HIGH -> default when a class is refused.
|
||||||
# best-effort — a failure just means the encode runs at default priority as it does today.
|
# `setcap -r` exits non-zero on a file that has no capability, hence the redirect.
|
||||||
if [ -x "$BIN" ]; then
|
if [ -x "$BIN" ]; then
|
||||||
if sudo setcap 'cap_sys_nice=ep' "$BIN" 2>/dev/null; then
|
sudo setcap -r "$BIN" 2>/dev/null || true
|
||||||
ok "granted CAP_SYS_NICE (PyroWave encode can outrank a GPU-bound game)"
|
|
||||||
else
|
|
||||||
warn "could not grant CAP_SYS_NICE to $BIN — PyroWave encode stays at default GPU priority"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
# SteamOS A/B updates rebuild /etc and DROP everything not on Valve's keep list — verified
|
# SteamOS A/B updates rebuild /etc and DROP everything not on Valve's keep list — verified
|
||||||
# live: an OS update stripped the udev rule + vhci autoload + UDP sysctl (gamepads silently
|
# live: an OS update stripped the udev rule + vhci autoload + UDP sysctl (gamepads silently
|
||||||
|
|||||||
Reference in New Issue
Block a user