Files
punktfunk/packaging/gamescope/patches/0005-punktfunk-stamp-the-version-banner-with-pfhdrN.patch
enricobuehler fdef4c90ce
ci / bun-nix (pull_request) Successful in 27s
ci / web (pull_request) Successful in 1m7s
ci / docs-site (pull_request) Successful in 1m20s
ci / rust-arm64 (pull_request) Successful in 1m31s
ci / rust (pull_request) Successful in 7m25s
fix(gamescope): stop the PipeWire use-after-free that aborted a session on every connect
A managed gamescope session on Nobara 44 (VM 123) died on essentially every
client connect. The visible symptom was a black screen; underneath,
`punktfunk-gamescope` was SIGABRT crash-looping — 11 coredumps in three minutes
— until `gamescope-session-plus` ran out of retries and came up on the *stock*
`/usr/bin/gamescope` at its default 1920x1080, which looks like a working game
mode and carries none of our capture patches.

    punktfunk-gamescope: ../src/pipewire.cpp:88: void destroy_buffer(
      pipewire_buffer*): Assertion `false' failed.
    #4  __assert_fail
    #5  destroy_buffer(pipewire_buffer*).cold

The abort is a use-after-free wearing an `assert(false); // unreachable`.
`pw_buffer->user_data` is associated with its `pipewire_buffer` in exactly one
place, at the bottom of `stream_handle_add_buffer` — after all four `goto error`
paths, whose label is a bare `delete buffer`. And `stream_handle_remove_buffer`
clears `buffer->buffer`, the only route back to the `pw_buffer`, while a still-
`copying` buffer is deleted later on the steamcompmgr thread with no way to
reach the slot. PipeWire recycles `pw_buffer` slots across renegotiations, so
the next remove reads `buffer->type` out of freed memory, falls off the end of
the switch and aborts.

The host sets the session to the client's mode on connect, and that mode change
is what renegotiates the stream — which is why "every connect" was the trigger.

Patch 0007 fixes the association rather than the symptom: set `user_data` at
allocation so it is valid on every path out of `add_buffer` and clear it on the
error path; clear it in `remove_buffer`, the last point both halves are known;
null-check the two consumers. The `default:` arm then logs instead of aborting.
Offered upstream — nothing about it is punktfunk-specific.

Two traps this cost time on, both now written down in the README:

  * It is NOT HDR-specific. The abort was first seen right after a 10-bit
    stream negotiated, so `PUNKTFUNK_GAMESCOPE_HDR=0` looked like a workaround.
    The failing argv carries no `--hdr-enabled` at all.
  * `gamescope-session-plus` hides it by falling back to stock gamescope, so a
    session existing proves nothing — read the banner.

`.pfhdrN` moves to 5 even though no capability moved: every deployed pfhdr4
binary crash-loops, so an operator needs to be able to tell them apart. All
`>=` thresholds in the host's probe are unaffected.

Also documents `libstdc++-static` as a build dependency — it is punktfunk's
requirement (the script links the C++ runtime statically on purpose), so no
`dnf builddep` will ever pull it, and without it meson fails with a message
naming neither the flag nor the package.

Verified on VM 123 with the patched binary installed: 5 rapid connect/
disconnect cycles plus 3 further sessions, zero new gamescope coredumps (43
before, 43 after), Steam game mode streaming real content at 5120x1440, and
`/tmp/chimeraos-short-session-tracker` never created — the short-session latch
that used to strand the box in plasma was downstream of this crash.
2026-08-10 02:16:22 +02:00

49 lines
2.5 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Enrico=20B=C3=BChler?= <enrico.buehler@unom.io>
Date: Sat, 8 Aug 2026 13:17:37 +0000
Subject: [PATCH] punktfunk: stamp the version banner with +pfhdrN
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
punktfunk decides a session's shape before the virtual display exists — the
bit depth in the Welcome (irrevocable; a PQ stream on an 8-bit encoder is a
hard error), whether it must composite the cursor host-side before the encoder
is even opened, and which flags the spawn has to carry. All of those have to be
static properties of the resolved binary rather than something negotiated later.
The number is a monotonic patch-set revision, so one probe answers all of them:
+pfhdr1 10-bit BT.2020/PQ capture formats
+pfhdr2 …and --pipewire-composite-cursor
+pfhdr3 …and the headless connector advertises its mode + --custom-refresh-rates
+pfhdr4 …and --pipewire-composite-external-overlay
+pfhdr5 …and the PipeWire buffer use-after-free is fixed (no new capability)
NOT for upstream: drop this once the functional patches land there and plain
version floors answer the same questions.
---
src/meson.build | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/src/meson.build b/src/meson.build
index 662f752..12fd38a 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -177,7 +177,15 @@ compiler_version = cc.version()
vcs_tag_cmd = ['git', 'describe', '--always', '--tags', '--dirty=+']
vcs_tag = run_command(vcs_tag_cmd, check: false).stdout().strip()
-version_tag = vcs_tag + ' (' + compiler_name + ' ' + compiler_version + ')'
+# punktfunk carries patches on top of upstream; stamp the banner so a host can probe what this
+# binary can do from `gamescope --version` alone, before it has to commit a session to it (the
+# punktfunk/1 Welcome is irrevocable). The number is a monotonic PATCH-SET revision:
+# +pfhdr1 — 10-bit BT.2020/PQ capture formats on the PipeWire node
+# +pfhdr2 — …and `--pipewire-composite-cursor`
+# +pfhdr3 — …and the headless connector advertises its mode + `--custom-refresh-rates`
+# +pfhdr4 — …and `--pipewire-composite-external-overlay`
+# +pfhdr5 — …and the PipeWire buffer use-after-free is fixed (no new capability)
+version_tag = vcs_tag + '+pfhdr5' + ' (' + compiler_name + ' ' + compiler_version + ')'
gamescope_version_conf = configuration_data()
gamescope_version_conf.set('VCS_TAG', version_tag)