A managed gamescope session 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.
pw_buffer->user_data and its pipewire_buffer are associated in exactly one place — the bottom of stream_handle_add_buffer — but that assignment sits after all four goto error paths, whose label is a bare delete buffer. Any failing add leaves the slot holding either nothing or the previous generation's freed pointer.
The teardown path leaks the same association even when nothing fails. stream_handle_remove_buffer clears buffer->buffer, the only route from the pipewire_buffer back to its pw_buffer. If the buffer is still copying it is not deleted there at all — the steamcompmgr thread deletes it later, by which point the pw_buffer can no longer be reached to be cleared.
PipeWire recycles struct pw_buffer slots across renegotiations, so the next remove_buffer on a recycled slot dereferences freed memory. buffer->type reads as garbage, falls off the end of the switch, and hits default: assert(false). The assert(buffer->buffer == nullptr) at the top passes on the way in, which is what makes this look like an "unreachable" assert rather than the use-after-free it is.
Why "every connect": the host sets the session to the client's mode, and that mode change is what renegotiates the stream.
The fix
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, now written into 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.
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.
.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 host probes are >=, so the bump is safe.
Validation
On-glass, home-nobara-1 (Nobara 44, RTX 5070 Ti):
5 rapid connect/disconnect cycles + 3 further sessions + a cold boot
43 gamescope coredumps before, 43 after — zero new
Steam game mode streaming real content at 5120x1440
/tmp/chimeraos-short-session-tracker never created — the short-session latch that used to strand the box in plasma was downstream of this crash
Regression-checked on Bazzite 43 (home-bazzite-2), built in a fedora:43 container: the pfhdr5 binary was confirmed to be the process actually running, session streamed 5120x1440@240, zero coredumps, HDR path intact — same as the pfhdr4 baseline.
⚠ Note for packaging: a gamescope binary is bound to its build host's libdisplay-info soname (fc44 links .so.3, Bazzite f43 has .so.2), so it must be built per-distro. It is not in the script's force_fallback_for list.
A managed gamescope session 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
```
## Root cause
`pw_buffer->user_data` and its `pipewire_buffer` are associated in exactly one place — the bottom of `stream_handle_add_buffer` — but that assignment sits **after all four `goto error` paths**, whose label is a bare `delete buffer`. Any failing add leaves the slot holding either nothing or the previous generation's freed pointer.
The teardown path leaks the same association even when nothing fails. `stream_handle_remove_buffer` clears `buffer->buffer`, the only route from the `pipewire_buffer` back to its `pw_buffer`. If the buffer is still `copying` it is not deleted there at all — the steamcompmgr thread deletes it later, by which point the `pw_buffer` can no longer be reached to be cleared.
PipeWire recycles `struct pw_buffer` slots across renegotiations, so the next `remove_buffer` on a recycled slot dereferences freed memory. `buffer->type` reads as garbage, falls off the end of the `switch`, and hits `default: assert(false)`. The `assert(buffer->buffer == nullptr)` at the top passes on the way in, which is what makes this look like an "unreachable" assert rather than the use-after-free it is.
**Why "every connect":** the host sets the session to the client's mode, and that mode change is what renegotiates the stream.
## The fix
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, now written into 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.
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.
`.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 host probes are `>=`, so the bump is safe.
## Validation
On-glass, home-nobara-1 (Nobara 44, RTX 5070 Ti):
* 5 rapid connect/disconnect cycles + 3 further sessions + a cold boot
* **43 gamescope coredumps before, 43 after — zero new**
* Steam game mode streaming real content at 5120x1440
* `/tmp/chimeraos-short-session-tracker` never created — the short-session latch that used to strand the box in plasma was downstream of this crash
Regression-checked on Bazzite 43 (home-bazzite-2), built in a fedora:43 container: the pfhdr5 binary was confirmed to be the process actually running, session streamed 5120x1440@240, zero coredumps, HDR path intact — same as the pfhdr4 baseline.
⚠ Note for packaging: a gamescope binary is bound to its build host's `libdisplay-info` soname (fc44 links `.so.3`, Bazzite f43 has `.so.2`), so it must be built per-distro. It is not in the script's `force_fallback_for` list.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A managed gamescope session died on essentially every client connect. The visible symptom was a black screen; underneath,
punktfunk-gamescopewas SIGABRT crash-looping — 11 coredumps in three minutes — untilgamescope-session-plusran out of retries and came up on the stock/usr/bin/gamescopeat its default 1920x1080, which looks like a working game mode and carries none of our capture patches.Root cause
pw_buffer->user_dataand itspipewire_bufferare associated in exactly one place — the bottom ofstream_handle_add_buffer— but that assignment sits after all fourgoto errorpaths, whose label is a baredelete buffer. Any failing add leaves the slot holding either nothing or the previous generation's freed pointer.The teardown path leaks the same association even when nothing fails.
stream_handle_remove_bufferclearsbuffer->buffer, the only route from thepipewire_bufferback to itspw_buffer. If the buffer is stillcopyingit is not deleted there at all — the steamcompmgr thread deletes it later, by which point thepw_buffercan no longer be reached to be cleared.PipeWire recycles
struct pw_bufferslots across renegotiations, so the nextremove_bufferon a recycled slot dereferences freed memory.buffer->typereads as garbage, falls off the end of theswitch, and hitsdefault: assert(false). Theassert(buffer->buffer == nullptr)at the top passes on the way in, which is what makes this look like an "unreachable" assert rather than the use-after-free it is.Why "every connect": the host sets the session to the client's mode, and that mode change is what renegotiates the stream.
The fix
Patch 0007 fixes the association rather than the symptom: set
user_dataat allocation so it is valid on every path out ofadd_bufferand clear it on the error path; clear it inremove_buffer, the last point both halves are known; null-check the two consumers. Thedefault:arm then logs instead of aborting. Offered upstream — nothing about it is punktfunk-specific.Two traps, now written into the README
PUNKTFUNK_GAMESCOPE_HDR=0looked like a workaround. The failing argv carries no--hdr-enabledat all.gamescope-session-plushides it by falling back to stock gamescope, so a session existing proves nothing — read the banner.Also documents
libstdc++-staticas a build dependency: it is punktfunk's requirement (the script links the C++ runtime statically on purpose), so nodnf builddepwill ever pull it, and without it meson fails with a message naming neither the flag nor the package..pfhdrNmoves 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 host probes are>=, so the bump is safe.Validation
On-glass, home-nobara-1 (Nobara 44, RTX 5070 Ti):
/tmp/chimeraos-short-session-trackernever created — the short-session latch that used to strand the box in plasma was downstream of this crashRegression-checked on Bazzite 43 (home-bazzite-2), built in a fedora:43 container: the pfhdr5 binary was confirmed to be the process actually running, session streamed 5120x1440@240, zero coredumps, HDR path intact — same as the pfhdr4 baseline.
⚠ Note for packaging: a gamescope binary is bound to its build host's
libdisplay-infosoname (fc44 links.so.3, Bazzite f43 has.so.2), so it must be built per-distro. It is not in the script'sforce_fallback_forlist.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.