#!/usr/bin/env bash # WP3 — the on-glass validation kit for `punktfunk-encode-worker`. # # Plan: punktfunk-planning/design/gpu-priority-capability-worker-implementation-plan.md §2/WP3. # This script is the mechanical half of that table. It runs ON a real box (Deck desktop mode, .21, # .25, .41, a nobara VM); it needs a GPU, and for the KDE legs a live KDE session. Nothing here # talks to another machine. # # WHAT IS BEING VALIDATED. `punktfunk-encode-worker` is a separate binary carrying # `cap_sys_nice=ep`; it encodes PyroWave at an elevated VK_KHR_global_priority class so the encode # dispatch can preempt a GPU-bound game. `punktfunk-host` carries NO capability, ever: KWin # identifies a Wayland client by resolving its /proc//exe, 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 holds no capabilities — so a capability-carrying host is # unidentifiable, `zkde_screencast_unstable_v1` is never advertised, and every KDE desktop session # loses streaming. That is the 0.26.0-1 field incident, and V1 below is its regression test. # # SAFE BY DEFAULT. With no arguments this only READS: /proc, getcap, the installed .desktop. It # starts no session, kills nothing, and never calls setcap. Every leg that runs a capture+encode # session must be named explicitly; the one leg that kills a process needs --allow-mutate on top. # This script NEVER setcaps anything, least of all the host — it asserts the host is uncapped and # hard-fails if it is not, which is the entire point of V1. # # SKIPPED IS NOT A PASS. A leg that cannot run prints SKIP with the reason and the exit code says # so (2). Only PASS counts, and anything this script cannot determine is reported, never assumed. # # Usage: scripts/validate-encode-worker.sh [LEG ...] [OPTIONS] # scripts/validate-encode-worker.sh recipe # the per-box human recipe, incl. sudo # scripts/validate-encode-worker.sh --help # # Legs: # inspect (default) pure read-only inspection: the capability matrix as it exists on THIS box, # plus every running host's runtime capability state. No session, no spawn. # v1 the 0.26.0-1 regression test — KDE only. # v2 grant: the worker reports REALTIME. # v3a IPC hop cost: in-process vs an UNCAPPED worker, both at default GPU priority. # R1's pre-registered abandonment gate. Needs no capability and no KDE. # v3b lever benefit: in-process at default priority vs the CAPPED worker. # v4 the fallback ladder (chaos). Read-only rungs by default; --allow-mutate adds kill -9. # v5 fd hygiene over a long session (default 10 min). # auto inspect + v1 + v2 + v3a + v4 — everything that needs no extra permission. # all auto + v3b + v5. # recipe the per-box human recipe, including the privileged steps this script will not run. # selftest red-team this kit's own readers against synthetic logs. No box, no GPU, no Linux — # run it before trusting a green run, because an assertion that cannot fail is not one. # # ⚠ USE INSTALLED PATHS ON THE KDE LEGS. KWin caches the grant per EXECUTABLE PATH, matched against # an installed .desktop's `Exec=`. A binary run from a scratch build directory is a different # path, so KWin refuses it and `zkde_screencast_unstable_v1` never appears. That is identification # WORKING, not a bug — do not go debugging KWin. This script resolves the host binary from the # installed .desktop precisely so the legs run against the path KWin knows. # # ⚠ THE WORKER'S PERF SUMMARY GOES TO INHERITED STDERR. The p50/p99 lines are emitted by the worker # process, whose stderr is the host's. They reach `journalctl`; they do NOT reach the web # console's Logs tab (that ring is a tracing layer INSIDE the host process). This kit runs the # spike itself and captures stderr directly, so it never depends on either. # No `set -e`. This harness deliberately runs commands that are EXPECTED to fail (a readlink that # must be refused, a worker that must not spawn); under -e a normal negative result aborts the run # and looks like a crash. Every fallible call below checks its own status instead. set -uo pipefail VERSION='WP3 kit 1' SELF="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" # --- the capability vocabulary, reused rather than re-derived ------------------------------------ # scripts/ci/assert-cap-matrix.sh (WP4) is sourceable by design and owns `caps_norm` — the # canonicalizer for getcap's two output forms ("path cap_sys_nice=ep" since libcap ~2.36, # "path = cap_sys_nice+ep" before) and rpm's "(none)". Reuse it: two normalizers that disagree is # exactly how a wrong capability string gets waved through. # # It carries `set -euo pipefail` at the top, which sourcing would import into THIS shell, so the # option set is saved and restored around the source. CAP_MATRIX_SH="$SELF/ci/assert-cap-matrix.sh" if [ ! -r "$CAP_MATRIX_SH" ]; then echo "FATAL: cannot read $CAP_MATRIX_SH — run this from a Punktfunk checkout (it owns the" >&2 echo " capability-string canonicalizer this kit reuses)." >&2 exit 3 fi _saved_opts="$(set +o)" # shellcheck source=scripts/ci/assert-cap-matrix.sh . "$CAP_MATRIX_SH" eval "$_saved_opts" unset _saved_opts set +e # `eval` above restored -e from the sourced file's perspective; this is our posture. WORKER_BIN_NAME='punktfunk-encode-worker' HOST_BIN_NAME='punktfunk-host' # CAP_SYS_NICE is capability bit 23, so the kernel prints it as CapPrm: 0000000000800000. CAP_SYS_NICE_BIT=23 # --- the log lines this kit asserts on, verbatim ------------------------------------------------- # Every one of these is a fixed substring of a real `tracing` message (grep -F, never a regex — the # messages contain em dashes and parentheses). If a message is reworded, this block is the single # place to fix, and a leg that stops matching FAILS rather than silently passing. L_FALLBACK='encoding in-process at default GPU priority' # the clause EVERY fallback ends with L_OFF='pyrowave: PUNKTFUNK_ENCODE_WORKER=off' L_NOTFOUND='the encode worker was not found beside the host binary or on PATH' L_NOTUP='the encode worker did not come up' L_GRANTED='encoding in the capability-carrying worker at an elevated global queue priority' L_NOPRIO='encoding in the capability-carrying worker, no queue priority requested' L_INERT='every global queue priority class was refused' L_INERT_WORKER='CAP_SYS_NICE on the encode WORKER binary' # the worker-side wording (host logs it) L_INERT_HOST='CAP_SYS_NICE on the host binary' # the in-process wording L_LEAVING='pyrowave: leaving the encode worker' L_DIED='the encode worker died mid-session' L_RESPAWNED='respawned the encode worker after a mid-session death' L_NORESPAWN='the encode worker would not respawn' L_WORKER_READY='punktfunk-encode-worker ready' L_PERF='pyrowave encode, submit->AU' L_CAPTURE='capture pipeline resolved:' L_KWIN_DENIED='does not expose zkde_screencast_unstable_v1' # --- options ------------------------------------------------------------------------------------- OPT_HOST_BIN='' OPT_WORKER_BIN='' OPT_HOST_PID='' OPT_SOURCE='' OPT_SECONDS=45 OPT_MINUTES=10 OPT_WIDTH=1920 OPT_HEIGHT=1080 OPT_FPS=60 OPT_BITRATE=20 OPT_GATE_MS='1.0' OPT_FD_TOLERANCE=0 OPT_ALLOW_MUTATE=0 OPT_KEEP_LOGS=0 OPT_LOG_DIR='' OPT_LOOPBACK=0 LEGS=() usage() { sed -n '2,56p' "${BASH_SOURCE[0]}" | sed -e 's/^# \{0,1\}//' cat <<'EOF' Options: --host-bin PATH the INSTALLED punktfunk-host (default: from a running host, then the installed .desktop's Exec=, then PATH) --worker-bin PATH the INSTALLED punktfunk-encode-worker (default: $PUNKTFUNK_ENCODE_WORKER, then beside the host binary, then PATH — mirrors the host's own resolution) --host-pid PID inspect this host process only (default: every running punktfunk-host) --source SRC spike capture source: kwin-virtual | portal (default: kwin-virtual under KWin, portal elsewhere). ⚠ synthetic is NOT usable — see the note in v3a. --seconds N wall clock per measured arm (default 45) --minutes N v5 session length (default 10) --width/--height/--fps N spike geometry (default 1920x1080@60) --bitrate MBPS spike bitrate (default 20) --gate-ms X v3a abandonment gate, p99 headroom in ms (default 1.0) --fd-tolerance N v5 allowed fd growth in the steady-state window (default 0) --with-loopback keep the spike's punktfunk-core loopback on (default: --no-loopback, less CPU noise in the latency arms) --allow-mutate permit the legs that change the box: v4's `kill -9` of a live worker --log-dir DIR where the captured spike logs go (default: a mktemp dir) --keep-logs do not delete the captured logs on exit -h, --help this text Exit: 0 every attempted leg PASSed and none was skipped · 1 at least one FAIL · 2 no FAIL but at least one SKIP (the run is incomplete — a skip is never a pass) · 3 the kit could not start. EOF } while [ $# -gt 0 ]; do case "$1" in inspect|v1|v2|v3a|v3b|v4|v5|auto|all) LEGS+=("$1") ;; recipe|selftest) LEGS+=("$1") ;; --host-bin) OPT_HOST_BIN="${2:-}"; shift ;; --worker-bin) OPT_WORKER_BIN="${2:-}"; shift ;; --host-pid) OPT_HOST_PID="${2:-}"; shift ;; --source) OPT_SOURCE="${2:-}"; shift ;; --seconds) OPT_SECONDS="${2:-}"; shift ;; --minutes) OPT_MINUTES="${2:-}"; shift ;; --width) OPT_WIDTH="${2:-}"; shift ;; --height) OPT_HEIGHT="${2:-}"; shift ;; --fps) OPT_FPS="${2:-}"; shift ;; --bitrate) OPT_BITRATE="${2:-}"; shift ;; --gate-ms) OPT_GATE_MS="${2:-}"; shift ;; --fd-tolerance) OPT_FD_TOLERANCE="${2:-}"; shift ;; --with-loopback) OPT_LOOPBACK=1 ;; --allow-mutate) OPT_ALLOW_MUTATE=1 ;; --log-dir) OPT_LOG_DIR="${2:-}"; shift ;; --keep-logs) OPT_KEEP_LOGS=1 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument '$1' (try --help)" >&2; exit 3 ;; esac shift done [ ${#LEGS[@]} -gt 0 ] || LEGS=(inspect) # Numeric options are used in arithmetic; a typo must be a refusal, not a bash error mid-leg. for _n in OPT_SECONDS OPT_MINUTES OPT_WIDTH OPT_HEIGHT OPT_FPS OPT_BITRATE OPT_FD_TOLERANCE; do case "${!_n}" in ''|*[!0-9]*) echo "FATAL: --${_n#OPT_} must be a whole number, got '${!_n}'" >&2; exit 3 ;; esac done case "$OPT_GATE_MS" in ''|*[!0-9.]*) echo "FATAL: --gate-ms must be numeric, got '$OPT_GATE_MS'" >&2; exit 3 ;; esac unset _n # --- result bookkeeping --------------------------------------------------------------------------- N_PASS=0; N_FAIL=0; N_SKIP=0 RESULTS=() C_OK=''; C_BAD=''; C_WARN=''; C_OFF='' if [ -t 1 ]; then C_OK=$'\033[32m'; C_BAD=$'\033[31m'; C_WARN=$'\033[33m'; C_OFF=$'\033[0m'; fi pass() { N_PASS=$((N_PASS+1)); RESULTS+=("PASS|$1|$2"); printf ' %sPASS%s %-6s %s\n' "$C_OK" "$C_OFF" "$1" "$2"; } fail() { N_FAIL=$((N_FAIL+1)); RESULTS+=("FAIL|$1|$2"); printf ' %sFAIL%s %-6s %s\n' "$C_BAD" "$C_OFF" "$1" "$2"; } skip() { N_SKIP=$((N_SKIP+1)); RESULTS+=("SKIP|$1|$2"); printf ' %sSKIP%s %-6s %s\n' "$C_WARN" "$C_OFF" "$1" "$2"; } info() { printf ' %s\n' "$*"; } head2() { printf '\n%s\n' "$*"; } # --- capability readers --------------------------------------------------------------------------- # The canonical capability string of a FILE, or the marker `?` when it genuinely could not be read. # `?` is never treated as "none" — a blind read that reports "no capability" would wave through the # exact state V1 exists to catch. file_caps() { local f="$1" raw [ -e "$f" ] || { printf '?'; return 1; } if command -v getcap >/dev/null 2>&1; then raw="$(getcap "$f" 2>/dev/null)" # getcap prints nothing at all for a file with no capability, and exits 0 — so an empty read # here is a real "none", not a failed read. if [ -z "$raw" ]; then printf ''; return 0; fi caps_norm "$(printf '%s' "$raw" | sed -e 's/^[^ ]* //' )" return 0 fi if command -v getfattr >/dev/null 2>&1; then # Fallback that at least distinguishes present-vs-absent, even though it cannot render flags. if getfattr -n security.capability --only-values -- "$f" >/dev/null 2>&1; then printf 'cap_present_unreadable'; return 0 fi printf ''; return 0 fi printf '?'; return 1 } # The PERMITTED capability mask of a live process, as the kernel's 16-hex-digit word, or '' if the # field could not be read. /proc//status is world-readable and carries CapPrm unconditionally, # so this works across a capability boundary where a readlink of /proc//exe does not. proc_capprm() { local pid="$1" line line="$(grep -m1 '^CapPrm:' "/proc/$pid/status" 2>/dev/null)" [ -n "$line" ] || { printf ''; return 1; } printf '%s' "$line" | awk '{print $2}' } capprm_is_zero() { case "${1:-}" in ''|*[!0]*) return 1 ;; *) return 0 ;; esac; } # Bit test against a 16-hex-digit mask. Only the low 32 bits are converted, which is both enough # (CAP_SYS_NICE is bit 23) and immune to the signed-64-bit overflow a full-width $((16#...)) invites. capprm_has_bit() { local hex="${1:-}" bit="${2:-0}" low [ -n "$hex" ] || return 2 case "$hex" in *[!0-9a-fA-F]*) return 2 ;; esac low="${hex: -8}" (( (16#$low >> bit) & 1 )) } # --- process discovery ---------------------------------------------------------------------------- host_pids() { if [ -n "$OPT_HOST_PID" ]; then printf '%s\n' "$OPT_HOST_PID"; return 0; fi # -x matches `comm`, which for a normally-exec'd host IS punktfunk-host. pgrep -x "$HOST_BIN_NAME" 2>/dev/null } # The worker processes spawned by a given host pid. # # ⚠ `pgrep -x punktfunk-encode-worker` does NOT find them. The host execs the worker through the # pinned `/proc/self/fd/` path (so a replaced binary cannot be swapped under a spawn), and the # kernel sets `comm` from the basename of the path passed to execve — i.e. a bare number. argv[0] # IS `punktfunk-encode-worker`, so a full-cmdline match works — but matching by PARENT is exact and # cannot pick up somebody else's worker, which matters a great deal before a `kill -9`. worker_pids_of() { local parent="$1" p cmd for p in $(pgrep -P "$parent" 2>/dev/null); do cmd="$(tr '\0' ' ' < "/proc/$p/cmdline" 2>/dev/null)" case "$cmd" in *"$WORKER_BIN_NAME"*) printf '%s\n' "$p" ;; esac done } fd_count() { local pid="$1" n n="$(ls "/proc/$pid/fd" 2>/dev/null | wc -l | tr -d ' ')" [ -n "$n" ] || { printf ''; return 1; } printf '%s' "$n" } # --- binary resolution ---------------------------------------------------------------------------- DESKTOP_FILE='' DESKTOP_EXEC='' find_desktop() { local d f for d in "${XDG_DATA_HOME:-$HOME/.local/share}/applications" \ /usr/share/applications /usr/local/share/applications \ /var/lib/flatpak/exports/share/applications; do f="$d/io.unom.Punktfunk.Host.desktop" if [ -r "$f" ]; then DESKTOP_FILE="$f" DESKTOP_EXEC="$(sed -n 's/^Exec=//p' "$f" | head -1 | awk '{print $1}')" return 0 fi done return 1 } HOST_BIN=''; HOST_BIN_FROM='' WORKER_BIN=''; WORKER_BIN_FROM='' resolve_binaries() { local pid if [ -n "$OPT_HOST_BIN" ]; then HOST_BIN="$OPT_HOST_BIN"; HOST_BIN_FROM='--host-bin' else for pid in $(host_pids); do HOST_BIN="$(readlink -f "/proc/$pid/exe" 2>/dev/null)" [ -n "$HOST_BIN" ] && { HOST_BIN_FROM="the running host, pid $pid"; break; } done if [ -z "$HOST_BIN" ] && find_desktop && [ -n "$DESKTOP_EXEC" ]; then HOST_BIN="$DESKTOP_EXEC"; HOST_BIN_FROM="$DESKTOP_FILE (Exec=)" fi if [ -z "$HOST_BIN" ]; then HOST_BIN="$(command -v "$HOST_BIN_NAME" 2>/dev/null)" [ -n "$HOST_BIN" ] && HOST_BIN_FROM='PATH' fi fi # Absolute, always: these strings become argv[0] of a spawned process, and `env ./relative` is a # PATH lookup that fails rather than the file the operator meant. [ -n "$HOST_BIN" ] && HOST_BIN="$(readlink -f "$HOST_BIN" 2>/dev/null || printf '%s' "$HOST_BIN")" find_desktop >/dev/null 2>&1 # Mirror the host's own resolution order (pyrowave_remote::resolve_worker_path). if [ -n "$OPT_WORKER_BIN" ]; then WORKER_BIN="$OPT_WORKER_BIN"; WORKER_BIN_FROM='--worker-bin' elif [ -n "${PUNKTFUNK_ENCODE_WORKER:-}" ] && \ [ "$(printf '%s' "${PUNKTFUNK_ENCODE_WORKER}" | tr 'A-Z' 'a-z')" != off ]; then WORKER_BIN="$PUNKTFUNK_ENCODE_WORKER"; WORKER_BIN_FROM='$PUNKTFUNK_ENCODE_WORKER' elif [ -n "$HOST_BIN" ] && [ -f "$(dirname "$HOST_BIN")/$WORKER_BIN_NAME" ]; then WORKER_BIN="$(dirname "$HOST_BIN")/$WORKER_BIN_NAME"; WORKER_BIN_FROM='beside the host binary' else WORKER_BIN="$(command -v "$WORKER_BIN_NAME" 2>/dev/null)" [ -n "$WORKER_BIN" ] && WORKER_BIN_FROM='PATH' fi [ -n "$WORKER_BIN" ] && WORKER_BIN="$(readlink -f "$WORKER_BIN" 2>/dev/null || printf '%s' "$WORKER_BIN")" return 0 } # --- log assertions -------------------------------------------------------------------------------- logs_have() { LC_ALL=C grep -qF -- "$2" "$1" 2>/dev/null; } logs_first() { LC_ALL=C grep -m1 -F -- "$2" "$1" 2>/dev/null; } # Always a number, even for a log that does not exist — a bare `grep -c` prints nothing on a missing # file and `[ "" -ge 1 ]` is a bash syntax error, not a failed assertion. (The default has to be # applied in the SHELL: `sed 's/^$/0/'` sees no line at all on empty input and emits nothing.) logs_count() { local n; n="$(LC_ALL=C grep -cF -- "$2" "$1" 2>/dev/null | tr -dc '0-9')" printf '%s' "${n:-0}" } perf_windows() { logs_count "$1" "$L_PERF"; } # Perf windows that appear AFTER a marker line. This is what "the stream survived" actually means # for the chaos leg: a warn followed by silence is a dead session that logged politely. perf_windows_after() { LC_ALL=C awk -v m="$2" -v p="$L_PERF" \ 'index($0,m){seen=1; next} seen && index($0,p){n++} END{print n+0}' "$1" 2>/dev/null } # The single most important assertion in the measured legs: prove the arm is the arm it claims. # # A PyroWave session that receives a CPU frame pins itself in-process for the rest of the session # (a socket copy of 1080p BGRA is ~480 MB/s), so a `cpu → pyrowave` capture arm silently turns the # "worker" arm into a second in-process arm — an A/B of inline against inline that PASSES the gate # for entirely the wrong reason. This refuses that. assert_worker_arm() { local log="$1" tag="$2" rc=0 if ! logs_have "$log" "$L_GRANTED" && ! logs_have "$log" "$L_NOPRIO" && ! logs_have "$log" "$L_INERT_WORKER"; then info "$tag: no 'capability-carrying worker' line — the worker never took the session"; rc=1 fi if logs_have "$log" "$L_FALLBACK"; then info "$tag: a fallback rung fired: $(logs_first "$log" "$L_FALLBACK" | sed 's/^.*pyrowave: /pyrowave: /')"; rc=1 fi if logs_have "$log" "$L_LEAVING"; then info "$tag: the session LEFT the worker mid-run: $(logs_first "$log" "$L_LEAVING" | sed 's/^.*pyrowave: /pyrowave: /')"; rc=1 fi if logs_have "$log" "$L_CAPTURE" && ! LC_ALL=C grep -F -- "$L_CAPTURE" "$log" | grep -q 'dmabuf-passthrough'; then info "$tag: capture arm is not dmabuf-passthrough — $(logs_first "$log" "$L_CAPTURE" | sed 's/^.*capture pipeline/capture pipeline/')" info "$tag: a non-dmabuf frame pins the session in-process, so this arm is NOT the worker." rc=1 fi return $rc } assert_inline_arm() { local log="$1" tag="$2" rc=0 if ! logs_have "$log" "$L_OFF"; then info "$tag: no 'PUNKTFUNK_ENCODE_WORKER=off' line — this arm did not force the in-process path"; rc=1 fi if logs_have "$log" "$L_GRANTED" || logs_have "$log" "$L_NOPRIO"; then info "$tag: a 'capability-carrying worker' line is present — this arm is not in-process"; rc=1 fi return $rc } # --- the perf instrument ---------------------------------------------------------------------------- # # `PUNKTFUNK_PERF` makes the PyroWave encoder emit one summary line every ~2 s over >=30 frames: # frames=30 mean_us=… p50_us=… p99_us=… max_us=… depth=… pyrowave encode, submit->AU (…) # The measured quantity is submit→AU: CSC + encode + fence wait + packetize. Per PW1 the FIRST # window is warm-up and is dropped, and `encode_fps` is a VACUOUS metric here (a starved capture # makes both arms report the capture rate) — latency is the whole point. # # In worker mode the line comes from the WORKER process on inherited stderr; in-process it comes # from the host. Either way it lands in the log this kit captures. perf_p99_series() { LC_ALL=C grep -F -- "$L_PERF" "$1" 2>/dev/null | sed -n 's/.*p99_us=\([0-9][0-9]*\).*/\1/p'; } perf_p50_series() { LC_ALL=C grep -F -- "$L_PERF" "$1" 2>/dev/null | sed -n 's/.*p50_us=\([0-9][0-9]*\).*/\1/p'; } # LC_ALL=C on every awk here is load-bearing, not decoration: under a comma-decimal locale awk # PRINTS "6,40" for a millisecond figure and, worse, READS "1.0" as 1 — which would silently turn # the --gate-ms 1.0 abandonment gate into a 1 µs gate. Numbers in this kit are C-locale throughout. median_us() { LC_ALL=C sort -n | LC_ALL=C awk 'NF{a[++n]=$1} END{ if(!n) exit 1; if(n%2) print a[(n+1)/2]; else print int((a[n/2]+a[n/2+1])/2) }' } ms() { LC_ALL=C awk -v u="${1:-0}" 'BEGIN{ printf "%.2f", u/1000 }'; } # Median p99 (and p50) across all windows after the warm-up one. Prints "p50_us p99_us windows", # or nothing when there is not enough data to mean anything. perf_summary() { local log="$1" min_windows="${2:-3}" p99s p50s n m99 m50 p99s="$(perf_p99_series "$log" | tail -n +2)" p50s="$(perf_p50_series "$log" | tail -n +2)" n="$(printf '%s\n' "$p99s" | grep -c '[0-9]')" [ "$n" -ge "$min_windows" ] 2>/dev/null || return 1 m99="$(printf '%s\n' "$p99s" | median_us)" || return 1 m50="$(printf '%s\n' "$p50s" | median_us)" || return 1 printf '%s %s %s' "$m50" "$m99" "$n" } # --- the spike vehicle ------------------------------------------------------------------------------- LOG_DIR='' setup_logdir() { [ -n "$LOG_DIR" ] && return 0 if [ -n "$OPT_LOG_DIR" ]; then LOG_DIR="$OPT_LOG_DIR"; mkdir -p "$LOG_DIR" else LOG_DIR="$(mktemp -d "${TMPDIR:-/tmp}/pf-wp3.XXXXXX")"; fi return 0 } cleanup() { # Never leave a spike (and through PR_SET_PDEATHSIG, its worker) running because the kit was # interrupted — a stray capture session holds a virtual output and a GPU context. stop_spike [ -n "$LOG_DIR" ] || return 0 if [ "$OPT_KEEP_LOGS" = 1 ] || [ -n "$OPT_LOG_DIR" ]; then printf '\nlogs kept in %s\n' "$LOG_DIR" else rm -rf "$LOG_DIR" fi } trap cleanup EXIT trap 'printf "\ninterrupted\n" >&2; exit 130' INT TERM SPIKE_SOURCE='' resolve_source() { [ -n "$SPIKE_SOURCE" ] && return 0 if [ -n "$OPT_SOURCE" ]; then SPIKE_SOURCE="$OPT_SOURCE"; return 0; fi case "$(compositor_guess)" in kwin) SPIKE_SOURCE='kwin-virtual' ;; *) SPIKE_SOURCE='portal' ;; esac } compositor_guess() { # Cheap and honest: a name, never a claim of readiness (probe-compositor is the readiness check). if pgrep -x kwin_wayland >/dev/null 2>&1; then printf 'kwin'; return 0; fi case "${XDG_CURRENT_DESKTOP:-}" in *KDE*|*kde*) printf 'kwin'; return 0 ;; esac if pgrep -x gnome-shell >/dev/null 2>&1; then printf 'mutter'; return 0; fi if pgrep -x sway >/dev/null 2>&1; then printf 'sway'; return 0; fi if pgrep -x gamescope >/dev/null 2>&1; then printf 'gamescope'; return 0; fi printf 'unknown' } # run_spike ... # # Launches `punktfunk-host spike --codec pyrowave` with PUNKTFUNK_PERF armed, captures the combined # stderr of the host AND its worker (one inherited stream), waits at most `wall + grace`, then # terminates it. Echoes the log path. Returns non-zero only if the spike could not be started. # # Output goes to /dev/null: a 10-minute 1080p PyroWave dump is ~1.5 GB and nothing here reads the # bitstream. The loopback is off by default — less CPU noise in a latency A/B. SPIKE_LOG='' SPIKE_PID='' run_spike() { local arm="$1" wall="$2"; shift 2 local log="$LOG_DIR/$arm.log" kv SPIKE_LOG="$log"; SPIKE_PID='' local -a envs=(PUNKTFUNK_PERF=1) for kv in "$@"; do envs+=("$kv"); done local -a cmd=("$HOST_BIN" spike --codec pyrowave --source "$SPIKE_SOURCE" --width "$OPT_WIDTH" --height "$OPT_HEIGHT" --fps "$OPT_FPS" --bitrate "$OPT_BITRATE" --seconds "$wall" --out /dev/null) [ "$OPT_LOOPBACK" = 1 ] || cmd+=(--no-loopback) { printf '### %s\n### env: %s\n### cmd: %s\n' "$arm" "${envs[*]}" "${cmd[*]}" } > "$log" env "${envs[@]}" "${cmd[@]}" >>"$log" 2>&1 & SPIKE_PID=$! sleep 1 kill -0 "$SPIKE_PID" 2>/dev/null || { # It may have finished legitimately (unlikely in 1 s) or died on open. Let the caller's # assertions decide; report the start as OK either way. wait "$SPIKE_PID" 2>/dev/null SPIKE_PID='' return 0 } return 0 } # Wait out an arm: the spike's own `--seconds` budget plus a grace, then stop it regardless. The # grace matters because a frame-starved capture (PW1 saw ~2.5 fps under load) will never reach its # frame target — the wall clock, not the frame count, is what bounds a leg. wait_spike() { local wall=$(( $1 + 15 )) waited=0 [ -n "$SPIKE_PID" ] || return 0 while [ "$waited" -lt "$wall" ]; do kill -0 "$SPIKE_PID" 2>/dev/null || break sleep 1; waited=$((waited+1)) done stop_spike } stop_spike() { [ -n "$SPIKE_PID" ] || return 0 if kill -0 "$SPIKE_PID" 2>/dev/null; then kill -TERM "$SPIKE_PID" 2>/dev/null local t=0 while [ "$t" -lt 5 ] && kill -0 "$SPIKE_PID" 2>/dev/null; do sleep 1; t=$((t+1)); done kill -KILL "$SPIKE_PID" 2>/dev/null fi wait "$SPIKE_PID" 2>/dev/null SPIKE_PID='' } # An UNCAPPED copy of the worker binary, in a temp dir. This is how the kit gets a default-priority # worker without touching the box: a plain `cp` does NOT carry the `security.capability` xattr, so # the copy is uncapped by construction — no setcap, no sudo, nothing restored afterwards. The copy # is VERIFIED uncapped before use; an unverifiable copy is refused, never assumed. # # ⚠ It is also the only arm immune to R4: the real capped worker is AT_SECURE, so the dynamic loader # ignores LD_LIBRARY_PATH/LD_PRELOAD. On a box propped up by a loader shim (the .21 ffmpeg-9 # workaround) the copy will start and the capped original will not — a v3a PASS beside a v3b # spawn failure is that, not a regression. UNCAPPED_COPY='' make_uncapped_copy() { [ -n "$UNCAPPED_COPY" ] && return 0 [ -n "$WORKER_BIN" ] && [ -x "$WORKER_BIN" ] || return 1 setup_logdir local dst="$LOG_DIR/uncapped-$WORKER_BIN_NAME" cp -- "$WORKER_BIN" "$dst" 2>/dev/null || return 1 chmod 0755 "$dst" 2>/dev/null local c; c="$(file_caps "$dst")" case "$c" in '') : ;; # verified uncapped — what we want '?') return 2 ;; # could not read: refuse rather than assume *) return 3 ;; # the copy carried the capability (cp --preserve=xattr?): refuse esac # A noexec /tmp would make this arm fail for a reason that has nothing to do with the worker. "$dst" >/dev/null 2>&1 case $? in 126|127) return 4 ;; esac UNCAPPED_COPY="$dst" return 0 } # ================================================================================================== # LEGS # ================================================================================================== leg_inspect() { head2 "INSPECT — the capability matrix on this box (read-only)" # --- I1 the worker binary -------------------------------------------------------------------- if [ -z "$WORKER_BIN" ]; then skip I1 "no $WORKER_BIN_NAME found (env, beside the host, PATH) — the host encodes in-process" info "at default GPU priority. That is a legitimate best-effort state, not a failure; it is" info "also why every grant leg below will skip." elif [ ! -x "$WORKER_BIN" ]; then fail I1 "$WORKER_BIN exists but is not executable" else pass I1 "worker binary: $WORKER_BIN ($WORKER_BIN_FROM)" fi # --- I2 the host binary carries NO capability — the whole point ------------------------------ if [ -z "$HOST_BIN" ]; then fail I2 "could not determine the host binary — pass --host-bin. Refusing to report a PASS from" info "a check that read nothing." elif [ ! -e "$HOST_BIN" ]; then fail I2 "host binary $HOST_BIN does not exist" else local hc; hc="$(file_caps "$HOST_BIN")" case "$hc" in '') pass I2 "host binary carries NO capability: $HOST_BIN" ;; '?') fail I2 "could not read capabilities of $HOST_BIN (no getcap/getfattr) — a blind check" info "cannot clear the host, and this is the assertion the leg exists for." ;; *) fail I2 "THE 0.26.0-1 REGRESSION: $HOST_BIN carries '$hc'." info "KWin cannot readlink /proc//exe of a capability-carrying process, so it" info "cannot identify the host, never advertises zkde_screencast_unstable_v1, and every" info "KDE desktop session loses streaming. Repair: sudo setcap -r '$HOST_BIN'" info "The GPU-priority grant belongs on $WORKER_BIN_NAME and nowhere else." ;; esac fi # --- I3 the worker's capability ---------------------------------------------------------------- if [ -n "$WORKER_BIN" ] && [ -e "$WORKER_BIN" ]; then local wc; wc="$(file_caps "$WORKER_BIN")" case "$wc" in "$WANT_WORKER_CAPS") pass I3 "worker carries exactly $WANT_WORKER_CAPS — the lever is live" ;; '') skip I3 "worker carries NO capability — the GPU-preemption lever is INERT here." info "Legitimate for a source build; the packaging channels grant it at install time." info "By hand: sudo setcap 'cap_sys_nice=ep' '$WORKER_BIN' (a REBUILD is a new inode" info "and drops it again). v2/v3b need this; v3a deliberately does not." ;; '?') fail I3 "could not read capabilities of $WORKER_BIN — refusing to guess" ;; *) fail I3 "worker carries '$wc', expected exactly '$WANT_WORKER_CAPS' — over-granting a" info "capability-carrying binary is its own hazard; fix the packaging channel." ;; esac else skip I3 "no worker binary to inspect" fi # --- I4 host and worker are DIFFERENT files ------------------------------------------------------ # The plan's non-negotiable: never a hardlink, never a subcommand. A shared inode shares the file # capability and silently re-creates 0.26.0-1. if [ -n "$HOST_BIN" ] && [ -n "$WORKER_BIN" ] && [ -e "$HOST_BIN" ] && [ -e "$WORKER_BIN" ]; then local hi wi hi="$(stat -Lc '%d:%i' "$HOST_BIN" 2>/dev/null)" wi="$(stat -Lc '%d:%i' "$WORKER_BIN" 2>/dev/null)" if [ -z "$hi" ] || [ -z "$wi" ]; then skip I4 "no stat(1) that reports device:inode — cannot prove the two are separate files" elif [ "$hi" = "$wi" ]; then fail I4 "host and worker are THE SAME INODE ($hi) — a hardlink shares the file capability," info "which puts the capability on the host and re-creates 0.26.0-1." else pass I4 "host and worker are separate files (inodes $hi / $wi)" fi else skip I4 "need both binaries to compare inodes" fi # --- I5 every RUNNING host is uncapped and identifiable ------------------------------------------ local pids; pids="$(host_pids)" if [ -z "$pids" ]; then skip I5 "no running $HOST_BIN_NAME — start one (or a session) to check the runtime state" else local pid ok=1 checked=0 for pid in $pids; do checked=$((checked+1)) local prm; prm="$(proc_capprm "$pid")" if [ -z "$prm" ]; then info "pid $pid: no CapPrm in /proc/$pid/status — cannot determine, treating as a failure"; ok=0; continue fi if ! capprm_is_zero "$prm"; then info "pid $pid: CapPrm=$prm — this host process HOLDS capabilities. KWin cannot identify it."; ok=0; continue fi local owner; owner="$(stat -c '%u' "/proc/$pid" 2>/dev/null)" if [ -n "$owner" ] && [ "$owner" != "$(id -u)" ]; then info "pid $pid: runs as uid $owner, this shell is uid $(id -u) — the /proc//exe read" info "pid $pid: is only meaningful from the session user KWin runs as. CapPrm=$prm is clean." continue fi if ! readlink "/proc/$pid/exe" >/dev/null 2>&1; then info "pid $pid: readlink /proc/$pid/exe REFUSED although CapPrm is zero — something else" info "pid $pid: (Yama ptrace_scope, an LSM) is blocking the read KWin identifies clients by."; ok=0; continue fi done if [ "$ok" = 1 ]; then pass I5 "$checked running host process(es): CapPrm all zero, /proc//exe readable" else fail I5 "a running host process is not identifiable — see above" fi fi # --- I6 the KWin .desktop grant ------------------------------------------------------------------- if [ -z "$DESKTOP_FILE" ]; then skip I6 "no io.unom.Punktfunk.Host.desktop installed — KWin has nothing to match the host" info "against, so zkde_screencast_unstable_v1 is never advertised however clean the capability" info "state is. On the Deck this file is written by scripts/steamdeck/install.sh; packaged" info "installs ship it. This is a MISSING PRECONDITION, not the capability regression." else local exec_ok=1 iface_ok=1 [ -n "$HOST_BIN" ] && [ "$DESKTOP_EXEC" != "$HOST_BIN" ] && exec_ok=0 grep -q '^X-KDE-Wayland-Interfaces=.*zkde_screencast_unstable_v1' "$DESKTOP_FILE" 2>/dev/null || iface_ok=0 if [ "$exec_ok" = 1 ] && [ "$iface_ok" = 1 ]; then pass I6 ".desktop grants the screencast interface to $DESKTOP_EXEC" else fail I6 "$DESKTOP_FILE does not authorize this host binary" [ "$exec_ok" = 0 ] && info "Exec=$DESKTOP_EXEC but the host binary is $HOST_BIN — KWin caches the" [ "$exec_ok" = 0 ] && info "grant per EXECUTABLE PATH, so a build run from anywhere else is refused." [ "$iface_ok" = 0 ] && info "X-KDE-Wayland-Interfaces does not list zkde_screencast_unstable_v1." fi fi } leg_v1() { head2 "V1 — the 0.26.0-1 regression test (KDE desktop session)" local comp; comp="$(compositor_guess)" if [ "$comp" != kwin ]; then skip V1 "compositor is '$comp', not KWin — V1 is the KWin identification gate and cannot run here" return 0 fi if [ -z "${WAYLAND_DISPLAY:-}" ]; then skip V1 "WAYLAND_DISPLAY is unset — run this from inside the KDE session, on glass" return 0 fi if [ -z "$DESKTOP_FILE" ]; then skip V1 "precondition missing: no io.unom.Punktfunk.Host.desktop (run scripts/steamdeck/install.sh" info "on the Deck, or install a package). Without it KWin cannot advertise the screencast" info "global for any reason, which would mask the capability question this leg exists to ask." return 0 fi if [ -z "$HOST_BIN" ] || [ ! -x "$HOST_BIN" ]; then skip V1 "no runnable host binary"; return 0 fi # The scratch-path trap, stated before it can be mis-debugged: KWin caches its grant per # EXECUTABLE PATH against the .desktop's Exec=. Probing a different build is a refusal by design. if [ -n "$DESKTOP_EXEC" ] && [ "$DESKTOP_EXEC" != "$HOST_BIN" ]; then skip V1 "the installed .desktop authorizes '$DESKTOP_EXEC', but this leg would probe '$HOST_BIN'." info "KWin would refuse the screencast global — correctly, because it identifies clients by" info "executable path. Run the INSTALLED binary (or pass --host-bin '$DESKTOP_EXEC'); do not" info "go debugging KWin." return 0 fi # The static half must already be clean; V1 restates it because a PASS here has to mean the # capability state is right, not just that a probe happened to answer. local hc; hc="$(file_caps "$HOST_BIN")" if [ "$hc" != '' ]; then fail V1 "host binary carries '${hc:-?}' — hard fail. See I2." return 0 fi # The instrument: probe-compositor connects as a KWin client from THIS binary and exits 0 only if # zkde_screencast_unstable_v1 was actually advertised to it. That is the identification path # end-to-end — the readlink, the .desktop match and the global — in one exit code. setup_logdir local plog="$LOG_DIR/v1-probe.log" "$HOST_BIN" probe-compositor >"$plog" 2>&1 local prc=$? if [ "$prc" != 0 ]; then fail V1 "probe-compositor exited $prc — KWin did not advertise the screencast global" if LC_ALL=C grep -qF -- "$L_KWIN_DENIED" "$plog"; then info "$(LC_ALL=C grep -m1 -F -- "$L_KWIN_DENIED" "$plog" | cut -c1-200)" fi info "If the message mentions CapPrm, this IS the 0.26.0-1 regression. If it does not, the" info ".desktop is missing/stale or KWin has cached a grant for a different path (re-login)." return 0 fi # …and a real capture+encode pass over that same grant, which is the "stream up" half. resolve_source run_spike v1-stream "$OPT_SECONDS" wait_spike "$OPT_SECONDS" local frames; frames="$(perf_windows "$SPIKE_LOG")" if [ "${frames:-0}" -lt 1 ]; then fail V1 "probe passed but the spike produced no encode windows — see $SPIKE_LOG" return 0 fi pass V1 "host uncapped, /proc//exe readable, zkde_screencast_unstable_v1 advertised, stream up" info "source=$SPIKE_SOURCE, $frames perf window(s). A client session on glass is the human half." } leg_v2() { head2 "V2 — grant: the worker reports an elevated global queue priority" if [ -z "$WORKER_BIN" ] || [ ! -x "$WORKER_BIN" ]; then skip V2 "no worker binary"; return 0 fi local wc; wc="$(file_caps "$WORKER_BIN")" if [ "$wc" != "$WANT_WORKER_CAPS" ]; then skip V2 "the worker carries '${wc:-}', not $WANT_WORKER_CAPS — every driver refuses every" info "elevated class without it, so a REFUSED result here would say nothing about the driver." return 0 fi [ -n "$HOST_BIN" ] && [ -x "$HOST_BIN" ] || { skip V2 "no runnable host binary"; return 0; } resolve_source setup_logdir # The intent is pinned rather than inherited: a host.env or shell with PYROWAVE_QUEUE_PRIORITY=off # in it would produce "no queue priority requested" and read as a failed grant. run_spike v2-grant "$OPT_SECONDS" "PUNKTFUNK_ENCODE_WORKER=$WORKER_BIN" "PYROWAVE_QUEUE_PRIORITY=realtime" wait_spike "$OPT_SECONDS" local log="$SPIKE_LOG" if ! assert_worker_arm "$log" V2; then fail V2 "the session did not run in the worker — see $log"; return 0 fi if logs_have "$log" "$L_INERT_WORKER"; then fail V2 "every global priority class was REFUSED even though the worker is capped." info "Either the driver does not honour VK_KHR_global_priority here, or the capability did not" info "survive to the running process. Check: $(logs_first "$log" "$L_WORKER_READY" | cut -c1-160)" return 0 fi local line; line="$(logs_first "$log" "$L_GRANTED")" if [ -z "$line" ]; then fail V2 "no grant line. Closest match: $(logs_first "$log" "$L_NOPRIO" | cut -c1-160)" info "PYROWAVE_QUEUE_PRIORITY=off would produce exactly that; unset it for this leg." return 0 fi case "$line" in *priority=Realtime*) pass V2 "Ready.granted = Realtime ($(printf '%s' "$line" | sed -n 's/.*device=\([^ ]*\).*/device=\1/p'))" ;; *priority=High*) fail V2 "granted HIGH, not REALTIME — the plan's criterion is REALTIME." info "A driver that caps at HIGH is a finding, not a kit failure; record it." ;; *) fail V2 "grant line carries no recognisable priority= field: $(printf '%s' "$line" | cut -c1-200)" ;; esac if logs_have "$log" "$L_WORKER_READY"; then info "worker: $(logs_first "$log" "$L_WORKER_READY" | sed 's/^.* INFO //' | cut -c1-180)" fi } # The shared A/B engine. $1 label, $2 arm-A log, $3 arm-B log, $4 gate µs (empty = report only). ab_report() { local tag="$1" a_log="$2" a_name="$3" b_log="$4" b_name="$5" gate_us="${6:-}" local a b a50 a99 an b50 b99 bn a="$(perf_summary "$a_log")" || { fail "$tag" "$a_name: fewer than 3 usable perf windows — the" info "capture may be starved, or PUNKTFUNK_PERF did not arm. Log: $a_log"; return 1; } b="$(perf_summary "$b_log")" || { fail "$tag" "$b_name: fewer than 3 usable perf windows. Log: $b_log"; return 1; } read -r a50 a99 an <<<"$a" read -r b50 b99 bn <<<"$b" info "$(printf '%-28s p50 %6s ms p99 %6s ms (%s windows)' "$a_name" "$(ms "$a50")" "$(ms "$a99")" "$an")" info "$(printf '%-28s p50 %6s ms p99 %6s ms (%s windows)' "$b_name" "$(ms "$b50")" "$(ms "$b99")" "$bn")" local delta=$((b99 - a99)) info "$(printf 'p99 delta (%s - %s): %s ms' "$b_name" "$a_name" "$(ms "$delta")")" [ -z "$gate_us" ] && return 0 if [ "$delta" -le "$gate_us" ]; then return 0; fi return 2 } leg_v3a() { head2 "V3a — IPC hop cost: in-process vs an UNCAPPED worker (R1's abandonment gate)" info "Both arms encode at DEFAULT GPU priority, so the only difference is the process boundary." info "The AU crosses via a memfd (pwrite/pread), not a socket copy, so this measures two small" info "JSON messages plus one memfd round trip per frame. Needs no capability and no KDE." info "⚠ --source synthetic CANNOT be used for this (or any) worker leg: a CPU-backed frame is" info "refused by the worker path on purpose (1080p BGRA is ~480 MB/s over the socket) and pins" info "the session in-process, so the 'worker' arm would silently be a second in-process arm." [ -n "$HOST_BIN" ] && [ -x "$HOST_BIN" ] || { skip V3a "no runnable host binary"; return 0; } if [ -z "$WORKER_BIN" ] || [ ! -x "$WORKER_BIN" ]; then skip V3a "no worker binary"; return 0; fi setup_logdir make_uncapped_copy case $? in 0) : ;; 2) skip V3a "cannot verify the working copy is uncapped (no getcap) — refusing to measure an arm" info "whose priority state is unknown"; return 0 ;; 3) skip V3a "the working copy carried the capability across — refusing to call it the uncapped arm"; return 0 ;; 4) skip V3a "the working copy is not executable where it was written (noexec ${TMPDIR:-/tmp}?) —" info "re-run with --log-dir pointing somewhere executable"; return 0 ;; *) skip V3a "could not copy $WORKER_BIN"; return 0 ;; esac resolve_source # Arm A: in-process, priority explicitly OFF so the two arms differ in nothing but the boundary. run_spike v3a-inline "$OPT_SECONDS" "PUNKTFUNK_ENCODE_WORKER=off" "PYROWAVE_QUEUE_PRIORITY=off" wait_spike "$OPT_SECONDS" local a_log="$SPIKE_LOG" # Arm B: the uncapped worker, same intent. run_spike v3a-worker "$OPT_SECONDS" "PUNKTFUNK_ENCODE_WORKER=$UNCAPPED_COPY" "PYROWAVE_QUEUE_PRIORITY=off" wait_spike "$OPT_SECONDS" local b_log="$SPIKE_LOG" assert_inline_arm "$a_log" V3a || { fail V3a "arm A is not the in-process arm — see $a_log"; return 0; } assert_worker_arm "$b_log" V3a || { fail V3a "arm B is not the worker arm — see $b_log"; return 0; } local gate_us; gate_us="$(LC_ALL=C awk -v m="$OPT_GATE_MS" 'BEGIN{ printf "%d", m*1000 }')" ab_report V3a "$a_log" "in-process (default prio)" "$b_log" "uncapped worker" "$gate_us" case $? in 0) pass V3a "the IPC hop costs <= ${OPT_GATE_MS} ms at p99 — R1's abandonment gate does NOT fire" ;; 2) fail V3a "THE PRE-REGISTERED ABANDONMENT GATE FIRED: the hop costs more than ${OPT_GATE_MS} ms" info "at p99. Per the plan, STOP and do the shm-ring AU return before any packaging ships." ;; *) : ;; # ab_report already recorded the failure esac } leg_v3b() { head2 "V3b — lever benefit: in-process at default priority vs the CAPPED worker" info "PW1's baseline on .21 (RTX 5070 Ti, GRID 2 at 54-87% GPU, 1080p): refused p99 ~6.4 ms," info "REALTIME p99 ~4.4 ms. This leg needs a GPU-BOUND LOAD to mean anything — an idle GPU has" info "nothing to preempt and both arms will look identical. Start the game first." [ -n "$HOST_BIN" ] && [ -x "$HOST_BIN" ] || { skip V3b "no runnable host binary"; return 0; } if [ -z "$WORKER_BIN" ] || [ ! -x "$WORKER_BIN" ]; then skip V3b "no worker binary"; return 0; fi local wc; wc="$(file_caps "$WORKER_BIN")" if [ "$wc" != "$WANT_WORKER_CAPS" ]; then skip V3b "the worker is not capped ($wc) — there is no lever to measure. See I3."; return 0 fi setup_logdir; resolve_source # Arm A is exactly PW1's "refused" arm: in-process, asking for priority and being refused (the # host binary is uncapped by construction, so the ladder walks REALTIME -> HIGH -> default). run_spike v3b-inline "$OPT_SECONDS" "PUNKTFUNK_ENCODE_WORKER=off" "PYROWAVE_QUEUE_PRIORITY=realtime" wait_spike "$OPT_SECONDS" local a_log="$SPIKE_LOG" run_spike v3b-worker "$OPT_SECONDS" "PUNKTFUNK_ENCODE_WORKER=$WORKER_BIN" "PYROWAVE_QUEUE_PRIORITY=realtime" wait_spike "$OPT_SECONDS" local b_log="$SPIKE_LOG" assert_inline_arm "$a_log" V3b || { fail V3b "arm A is not the in-process arm — see $a_log"; return 0; } assert_worker_arm "$b_log" V3b || { fail V3b "arm B is not the worker arm — see $b_log"; return 0; } if ! logs_have "$b_log" "$L_GRANTED"; then fail V3b "arm B did not get a grant — this would be an A/B of two default-priority arms"; return 0 fi if ! logs_have "$a_log" "$L_INERT_HOST"; then info "note: arm A did not log the in-process INERT warn; it may already be at default priority" info "for another reason (PYROWAVE_QUEUE_PRIORITY=off in this environment?)." fi ab_report V3b "$a_log" "in-process, refused" "$b_log" "capped worker, granted" case $? in 0) local a99 b99 a99="$(perf_summary "$a_log" | awk '{print $2}')"; b99="$(perf_summary "$b_log" | awk '{print $2}')" if [ "${b99:-0}" -lt "${a99:-0}" ]; then pass V3b "the capped worker's p99 is BELOW the refused arm's — the lever pays for the hop" else fail V3b "the capped worker's p99 is NOT below the refused arm's. On an idle GPU that is" info "expected and the leg is meaningless — re-run under a real GPU-bound load." fi ;; *) : ;; esac } leg_v4() { head2 "V4 — the fallback ladder: no rung may kill a negotiated session" [ -n "$HOST_BIN" ] && [ -x "$HOST_BIN" ] || { skip V4 "no runnable host binary"; return 0; } setup_logdir; resolve_source local short=$(( OPT_SECONDS < 20 ? OPT_SECONDS : 20 )) # --- rung: the binary is not there. Read-only — a path that does not exist, never a deletion. # An operator-set PUNKTFUNK_ENCODE_WORKER is deliberately NOT existence-checked (a named path is # entitled to a failure that names it back), so this lands on the SPAWN rung ("did not come up") # rather than the not-found one; both are accepted here because both are the same guarantee. run_spike v4-missing "$short" "PUNKTFUNK_ENCODE_WORKER=$LOG_DIR/definitely-not-here" wait_spike "$short" local log="$SPIKE_LOG" if logs_have "$log" "$L_NOTUP" || logs_have "$log" "$L_NOTFOUND"; then if [ "$(perf_windows "$log")" -ge 1 ]; then pass V4.a "a missing worker: one warn, encoding in-process, stream up" else fail V4.a "the warn fired but nothing encoded — see $log" fi else fail V4.a "no fallback warn for a worker path that does not exist — see $log" fi # --- rung: PUNKTFUNK_ENCODE_WORKER=off (the documented escape hatch). run_spike v4-off "$short" "PUNKTFUNK_ENCODE_WORKER=off" wait_spike "$short" log="$SPIKE_LOG" if logs_have "$log" "$L_OFF" && [ "$(perf_windows "$log")" -ge 1 ]; then pass V4.b "PUNKTFUNK_ENCODE_WORKER=off: one info line, encoding in-process, stream up" else fail V4.b "the off escape hatch did not behave — see $log" fi # --- rung: a worker that starts and immediately dies (spawn ok, handshake never completes). if [ -x /bin/false ]; then run_spike v4-false "$short" "PUNKTFUNK_ENCODE_WORKER=/bin/false" wait_spike "$short" log="$SPIKE_LOG" if logs_have "$log" "$L_NOTUP" && [ "$(perf_windows "$log")" -ge 1 ]; then pass V4.c "a worker that dies at handshake: one warn with error=, in-process, stream up" info "$(logs_first "$log" "$L_NOTUP" | sed -n 's/.*\(error=[^ ]*\).*/\1/p' | cut -c1-120)" else fail V4.c "a /bin/false worker did not produce the handshake-failure rung — see $log" fi else skip V4.c "no /bin/false to stand in for a worker that dies at handshake" fi # --- rung: the capability is absent → INERT warn, default priority, stream still up. # Uses the uncapped COPY, so nothing on the box is stripped and no sudo is needed. if [ -n "$WORKER_BIN" ] && [ -x "$WORKER_BIN" ] && [ "$(file_caps "$WORKER_BIN")" = "$WANT_WORKER_CAPS" ]; then make_uncapped_copy if [ $? = 0 ]; then run_spike v4-inert "$short" "PUNKTFUNK_ENCODE_WORKER=$UNCAPPED_COPY" "PYROWAVE_QUEUE_PRIORITY=realtime" wait_spike "$short" log="$SPIKE_LOG" if logs_have "$log" "$L_INERT" && logs_have "$log" "$L_INERT_WORKER" \ && [ "$(perf_windows "$log")" -ge 1 ]; then pass V4.d "an uncapped worker: the INERT warn names the WORKER binary, stream still up" if logs_have "$log" "$L_INERT_HOST"; then fail V4.d2 "the INERT warn ALSO fired with the host-binary wording — that sentence sends" info "an operator to setcap the host, which IS the 0.26.0-1 incident. It must not" info "double-fire while the worker is active." fi else fail V4.d "an uncapped worker did not produce the INERT warn + a live stream — see $log" fi else skip V4.d "could not produce a verified-uncapped worker copy" fi else skip V4.d "the worker is not capped here, so the INERT arm is already the normal state (see I3)" fi # --- rung: kill -9 mid-session. This one genuinely kills a process. if [ "$OPT_ALLOW_MUTATE" != 1 ]; then skip V4.e "kill -9 of a live worker needs --allow-mutate (it is the one step here that changes" info "the state of the box). It kills ONLY a worker that is a child of the spike this script" info "started — never a worker belonging to a real session." elif [ -z "$WORKER_BIN" ] || [ ! -x "$WORKER_BIN" ]; then skip V4.e "no worker binary to kill" else local kwall=$(( short * 3 )) run_spike v4-kill "$kwall" "PUNKTFUNK_ENCODE_WORKER=$WORKER_BIN" "PYROWAVE_QUEUE_PRIORITY=realtime" local spike_pid="$SPIKE_PID" wlog="$SPIKE_LOG" wpid='' t=0 while [ "$t" -lt 30 ] && [ -z "$wpid" ]; do wpid="$(worker_pids_of "$spike_pid" | head -1)" [ -n "$wpid" ] && break sleep 1; t=$((t+1)) done if [ -z "$wpid" ]; then stop_spike skip V4.e "no worker process appeared as a child of the spike within 30 s — nothing to kill" info "(the spike may have fallen back before spawning; check $wlog)" else sleep 5 # let it encode for a while first, so the death is genuinely mid-session kill -9 "$wpid" 2>/dev/null info "killed worker pid $wpid (child of spike pid $spike_pid)" sleep 8 local still=1; kill -0 "$spike_pid" 2>/dev/null || still=0 stop_spike local total after total="$(perf_windows "$wlog")"; after="$(perf_windows_after "$wlog" "$L_DIED")" if ! logs_have "$wlog" "$L_DIED"; then fail V4.e "no '$L_DIED' line after killing pid $wpid — see $wlog" elif [ "${after:-0}" -lt 1 ]; then fail V4.e "the death was logged but NOTHING encoded afterwards ($total window(s), all before" info "the kill) — the session did not actually survive. See $wlog" elif logs_have "$wlog" "$L_RESPAWNED"; then pass V4.e "kill -9 mid-session: one respawn, then $after more encode window(s) (host alive=$still)" elif logs_have "$wlog" "$L_NORESPAWN" || logs_have "$wlog" "$L_FALLBACK"; then pass V4.e "kill -9 mid-session: pinned in-process, $after more encode window(s), no dead stream" else fail V4.e "the death was logged and encoding continued, but neither a respawn nor an" info "in-process pin was logged — the ladder took an undeclared path. See $wlog" fi fi fi } leg_v5() { head2 "V5 — fd hygiene over a long session" info "The fd-identity cache passes a dmabuf's fds only on FIRST sight of its buffer key, and the" info "PipeWire pool recycles a small set, so steady state passes zero fds. A rising count is R2." [ -n "$HOST_BIN" ] && [ -x "$HOST_BIN" ] || { skip V5 "no runnable host binary"; return 0; } if [ -z "$WORKER_BIN" ] || [ ! -x "$WORKER_BIN" ]; then skip V5 "no worker binary"; return 0; fi setup_logdir; resolve_source local wall=$(( OPT_MINUTES * 60 )) run_spike v5-fd "$wall" "PUNKTFUNK_ENCODE_WORKER=$WORKER_BIN" local spike_pid="$SPIKE_PID" log="$SPIKE_LOG" if [ -z "$spike_pid" ]; then skip V5 "the spike did not stay up"; return 0; fi local wpid='' t=0 while [ "$t" -lt 30 ] && [ -z "$wpid" ]; do wpid="$(worker_pids_of "$spike_pid" | head -1)" [ -n "$wpid" ] && break sleep 1; t=$((t+1)) done if [ -z "$wpid" ]; then stop_spike skip V5 "no worker child appeared — the session fell back before spawning (see $log)" return 0 fi if [ -z "$(fd_count "$wpid")" ]; then stop_spike skip V5 "cannot read /proc/$wpid/fd. A capability-carrying process is normally opaque, which is" info "why the worker sets PR_SET_DUMPABLE(1) at startup — if this fails, that did not take." info "Re-run this leg as root to measure anyway, and treat the opacity itself as a finding." return 0 fi # The file capability, cross-checked at RUNTIME on the live worker. /proc//status is # world-readable and carries CapPrm unconditionally, so this reads across the capability boundary # that refuses a /proc//exe readlink. (That refusal is EXPECTED here and is not a problem: # nothing resolves the worker's exe — it speaks one socketpair to its parent and is not a KWin # client. It is only the HOST that must stay readable, which is I5.) local wprm; wprm="$(proc_capprm "$wpid")" if [ -z "$wprm" ]; then info "worker pid $wpid: no CapPrm field to read" elif capprm_has_bit "$wprm" "$CAP_SYS_NICE_BIT"; then info "worker pid $wpid: CapPrm=$wprm — CAP_SYS_NICE is live in the running process" else info "worker pid $wpid: CapPrm=$wprm — CAP_SYS_NICE is NOT in the running process's permitted" info "set, so the lever is inert however the file looks. A rebuild is a new inode and drops the" info "capability; re-run the installer or setcap the worker again." fi # Warm up, then sample. The first minute covers pool negotiation and the first sight of every # recycled buffer — the identity cache is only expected to be quiet AFTER that. local warm=60; [ "$wall" -gt 180 ] || warm=$(( wall / 3 )) info "warm-up ${warm}s, then sampling every 15 s for $(( wall - warm ))s (host pid $spike_pid, worker pid $wpid)" sleep "$warm" local hmin='' hmax='' wmin='' wmax='' hlast='' wlast='' n=0 elapsed="$warm" while [ "$elapsed" -lt "$wall" ]; do kill -0 "$spike_pid" 2>/dev/null || break kill -0 "$wpid" 2>/dev/null || { info "the worker exited at t=${elapsed}s"; break; } local h w h="$(fd_count "$spike_pid")"; w="$(fd_count "$wpid")" if [ -n "$h" ] && [ -n "$w" ]; then n=$((n+1)); hlast="$h"; wlast="$w" [ -z "$hmin" ] && { hmin="$h"; hmax="$h"; wmin="$w"; wmax="$w"; } [ "$h" -lt "$hmin" ] && hmin="$h"; [ "$h" -gt "$hmax" ] && hmax="$h" [ "$w" -lt "$wmin" ] && wmin="$w"; [ "$w" -gt "$wmax" ] && wmax="$w" printf ' t=%4ss host fd=%-5s worker fd=%s\n' "$elapsed" "$h" "$w" fi sleep 15; elapsed=$((elapsed+15)) done stop_spike if [ "$n" -lt 4 ]; then skip V5 "only $n samples — the session did not stay up long enough to say anything" return 0 fi local hg=$((hmax - hmin)) wg=$((wmax - wmin)) info "host fd min=$hmin max=$hmax last=$hlast growth=$hg" info "worker fd min=$wmin max=$wmax last=$wlast growth=$wg" if [ "$hg" -le "$OPT_FD_TOLERANCE" ] && [ "$wg" -le "$OPT_FD_TOLERANCE" ]; then pass V5 "fd counts stable across ${n} samples over $(( wall - warm ))s (tolerance $OPT_FD_TOLERANCE)" else fail V5 "fd count grew (host +$hg, worker +$wg) beyond the tolerance $OPT_FD_TOLERANCE — R2." info "Compare \`ls -l /proc/$wpid/fd\` at both ends of a run to see WHAT is accumulating." fi } # Red-teams this kit's own readers against synthetic logs, so the assertions are known to be able to # FAIL before anyone trusts a green run on a box. Pure bash + sed/awk: runs anywhere, including a # laptop with no GPU, no getcap and no Linux. leg_selftest() { local fails=0 d; d="$(mktemp -d "${TMPDIR:-/tmp}/pf-wp3-st.XXXXXX")" _t() { # _t