Each gamescope-backed session left a coredump behind. It fires after the compositor has finished its work — Primary child shut down!, then the crash — so the stream itself looked fine and this surfaced only as a steady drip of coredumps and a non-zero exit from the spawn. Five of them landed during one ordinary ten-minute session on the GNOME box.
What it actually is
Static destruction order. Not a race, and not anything gamescope does wrong at runtime.
g_device (CVulkanDevice) and g_output (VulkanOutput_t) were plain globals, so glibc ran their destructors from __run_exit_handlers once main() returned. Those destructors call back into the driver:
but the Vulkan ICD has already been torn down and unloaded by that point, so each call jumps through a function pointer into an unmapped page. The faulting address equalling the instruction pointer is the signature:
#0 0x00007fe8fd1d1070 in ?? ()
#1 CVulkanCmdBuffer::~CVulkanCmdBuffer at rendervulkan.cpp:1543
#9 std::vector<unique_ptr<CVulkanCmdBuffer>>::~vector (g_device+1792)
#10 CVulkanDevice::~CVulkanDevice at rendervulkan.hpp:768
#11 __run_exit_handlers / exit()
Reproducible in one line, no client and no GPU workload needed:
gamescope --backend headless -W 1280 -H 720 -r 60 --xwayland-count 1 -- true # exit 139, every time
The fix
Patch 0006 gives both globals storage that is constructed exactly as before but never destroyed — a union member is destroyed only if the union's destructor says so, and ours deliberately does not. Nothing needs freeing there: the process is exiting and the kernel reclaims the device, its command buffers and every GPU allocation. g_device and g_output keep their names and types (now references bound at constant-initialisation time), so no use site changes.
Both are needed. Pinning only the device relocated the fault into ~VulkanOutput_t, which is why this is a shared CNoDestroy<T> helper rather than a one-off.
Why +pfhdrN deliberately does not move
The marker is a capability tier the host probes via gamescope_patch_level()before it spawns. This patch adds no capability, so bumping it would advertise a tier that does not exist and strand hosts that gate on it. Per the PKGBUILD's own rule this ships as a pkgrel bump instead (1 → 2), and the README now spells that split out so the next patch does not have to rediscover it.
No packaging file needed editing otherwise: the PKGBUILD, the RPM spec and packaging/nix/gamescope.nix all glob patches/*.patch rather than enumerating them.
Verification
On an NVIDIA host, all six patches git am-ing onto the pinned upstream commit and then a release build — the shipped configuration, not the debug one used to find this:
version banner
3.16.25-7-gea635c1+pfhdr4 — marker intact
patched, real spawn shape (2752x2064@120 --steam --xwayland-count 1)
6/6 exit 0
distro control, same shape
SIGSEGV
Notes for whoever touches this next
Two things cost real time here and are worth knowing:
coredumpctl's inline backtrace is not trustworthy for a stripped binary. For this same crash it rendered four frames inside libnvidia-eglcore with a libstdc++/clone3 tail, which reads exactly like a worker thread crashing in EGL — a theory I pursued and which was wrong. gdb on the same core against a symbolised build showed the truth. Arch publishes no gamescope debuginfo (404 on debuginfod), so symbolising means building our own with --buildtype=debugoptimized.
--xwayland-count 0 is an unrelated bug and is left alone here: it dies far earlier, in main() at wlserver.cpp:3215, dereferencing a null gamescope_xwayland_server_t. It never reaches teardown. Punktfunk always spawns with --xwayland-count 1, so that path is never taken — but it briefly made the fix look ineffective when the real configuration was already clean.
Not filed upstream, per instruction, though the bug is not punktfunk-specific and the patch would apply as-is.
Each gamescope-backed session left a coredump behind. It fires *after* the compositor has finished its work — `Primary child shut down!`, then the crash — so the stream itself looked fine and this surfaced only as a steady drip of coredumps and a non-zero exit from the spawn. Five of them landed during one ordinary ten-minute session on the GNOME box.
## What it actually is
Static destruction order. Not a race, and not anything gamescope does wrong at runtime.
`g_device` (`CVulkanDevice`) and `g_output` (`VulkanOutput_t`) were plain globals, so glibc ran their destructors from `__run_exit_handlers` once `main()` returned. Those destructors call back into the driver:
```
~CVulkanCmdBuffer -> m_device->vk.FreeCommandBuffers(...)
~CVulkanTexture -> vk.Destroy*(...)
```
but the Vulkan ICD has already been torn down and unloaded by that point, so each call jumps through a function pointer into an unmapped page. The faulting address equalling the instruction pointer is the signature:
```
#0 0x00007fe8fd1d1070 in ?? ()
#1 CVulkanCmdBuffer::~CVulkanCmdBuffer at rendervulkan.cpp:1543
#9 std::vector<unique_ptr<CVulkanCmdBuffer>>::~vector (g_device+1792)
#10 CVulkanDevice::~CVulkanDevice at rendervulkan.hpp:768
#11 __run_exit_handlers / exit()
```
Reproducible in one line, no client and no GPU workload needed:
```
gamescope --backend headless -W 1280 -H 720 -r 60 --xwayland-count 1 -- true # exit 139, every time
```
## The fix
Patch `0006` gives both globals storage that is constructed exactly as before but never destroyed — a union member is destroyed only if the union's destructor says so, and ours deliberately does not. Nothing needs freeing there: the process is exiting and the kernel reclaims the device, its command buffers and every GPU allocation. `g_device` and `g_output` keep their names and types (now references bound at constant-initialisation time), so no use site changes.
Both are needed. Pinning only the device relocated the fault into `~VulkanOutput_t`, which is why this is a shared `CNoDestroy<T>` helper rather than a one-off.
## Why `+pfhdrN` deliberately does not move
The marker is a capability tier the host probes via `gamescope_patch_level()` *before* it spawns. This patch adds no capability, so bumping it would advertise a tier that does not exist and strand hosts that gate on it. Per the PKGBUILD's own rule this ships as a `pkgrel` bump instead (1 → 2), and the README now spells that split out so the next patch does not have to rediscover it.
No packaging file needed editing otherwise: the PKGBUILD, the RPM spec and `packaging/nix/gamescope.nix` all glob `patches/*.patch` rather than enumerating them.
## Verification
On an NVIDIA host, all six patches `git am`-ing onto the pinned upstream commit and then a **release** build — the shipped configuration, not the debug one used to find this:
| | |
|---|---|
| version banner | `3.16.25-7-gea635c1+pfhdr4` — marker intact |
| patched, real spawn shape (`2752x2064@120 --steam --xwayland-count 1`) | 6/6 exit 0 |
| distro control, same shape | SIGSEGV |
## Notes for whoever touches this next
Two things cost real time here and are worth knowing:
- **`coredumpctl`'s inline backtrace is not trustworthy for a stripped binary.** For this same crash it rendered four frames inside `libnvidia-eglcore` with a `libstdc++`/`clone3` tail, which reads exactly like a worker thread crashing in EGL — a theory I pursued and which was wrong. `gdb` on the same core against a symbolised build showed the truth. Arch publishes no gamescope debuginfo (404 on debuginfod), so symbolising means building our own with `--buildtype=debugoptimized`.
- **`--xwayland-count 0` is an unrelated bug** and is left alone here: it dies far earlier, in `main()` at `wlserver.cpp:3215`, dereferencing a null `gamescope_xwayland_server_t`. It never reaches teardown. Punktfunk always spawns with `--xwayland-count 1`, so that path is never taken — but it briefly made the fix look ineffective when the real configuration was already clean.
Not filed upstream, per instruction, though the bug is not punktfunk-specific and the patch would apply as-is.
Each gamescope-backed session left a coredump behind. It happened after the
compositor had finished its work — "Primary child shut down!", then the crash —
so the stream itself looked fine and it surfaced only as a steady drip of
coredumps and a non-zero exit from the spawn.
It is a static-destruction-order bug, not a race and not anything gamescope does
wrong at runtime. `g_device` (CVulkanDevice) and `g_output` (VulkanOutput_t) were
plain globals, so glibc ran their destructors from `__run_exit_handlers` once
main() returned. Those destructors call back into the driver —
`~CVulkanCmdBuffer` -> `vk.FreeCommandBuffers`, `~CVulkanTexture` -> `vk.Destroy*`
— but the Vulkan ICD has already been torn down and unloaded by then, so each
call jumps through a function pointer into an unmapped page. The faulting address
equalling the instruction pointer is the signature:
#0 0x00007fe8fd1d1070 in ?? ()
#1 CVulkanCmdBuffer::~CVulkanCmdBuffer at rendervulkan.cpp:1543
#9 std::vector<unique_ptr<CVulkanCmdBuffer>>::~vector (g_device+1792)
#10 CVulkanDevice::~CVulkanDevice at rendervulkan.hpp:768
#11 __run_exit_handlers / exit()
Patch 0006 gives both globals storage that is constructed exactly as before but
never destroyed; a union member is destroyed only if the union's destructor says
so, and ours deliberately does not. Nothing needs freeing there — the process is
exiting and the kernel reclaims the device, its command buffers and every GPU
allocation. Both objects are needed: pinning only the device relocated the fault
into ~VulkanOutput_t.
The `.pfhdrN` level deliberately stays at 4. It is a capability tier the host
probes before it spawns, and this patch adds no capability — bumping it would
advertise a tier that does not exist. Per the PKGBUILD's own rule this ships as a
`pkgrel` bump instead.
Verified on an NVIDIA box, all six patches `git am`-ing onto the pinned upstream
commit and then a RELEASE build (the shipped configuration):
version banner 3.16.25-7-gea635c1+pfhdr4 (marker intact)
patched, real spawn shape (2752x2064@120 --steam --xwayland-count 1)
6/6 exit 0
distro control, same shape
SIGSEGV
Not filed upstream, though it is not punktfunk-specific and would apply as-is.
Unrelated and left alone: `--xwayland-count 0` dies much earlier, in main() at
wlserver.cpp:3215, dereferencing a null `gamescope_xwayland_server_t`. Punktfunk
always spawns with `--xwayland-count 1`, so that path is never taken here.
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.
Each gamescope-backed session left a coredump behind. It fires after the compositor has finished its work —
Primary child shut down!, then the crash — so the stream itself looked fine and this surfaced only as a steady drip of coredumps and a non-zero exit from the spawn. Five of them landed during one ordinary ten-minute session on the GNOME box.What it actually is
Static destruction order. Not a race, and not anything gamescope does wrong at runtime.
g_device(CVulkanDevice) andg_output(VulkanOutput_t) were plain globals, so glibc ran their destructors from__run_exit_handlersoncemain()returned. Those destructors call back into the driver:but the Vulkan ICD has already been torn down and unloaded by that point, so each call jumps through a function pointer into an unmapped page. The faulting address equalling the instruction pointer is the signature:
Reproducible in one line, no client and no GPU workload needed:
The fix
Patch
0006gives both globals storage that is constructed exactly as before but never destroyed — a union member is destroyed only if the union's destructor says so, and ours deliberately does not. Nothing needs freeing there: the process is exiting and the kernel reclaims the device, its command buffers and every GPU allocation.g_deviceandg_outputkeep their names and types (now references bound at constant-initialisation time), so no use site changes.Both are needed. Pinning only the device relocated the fault into
~VulkanOutput_t, which is why this is a sharedCNoDestroy<T>helper rather than a one-off.Why
+pfhdrNdeliberately does not moveThe marker is a capability tier the host probes via
gamescope_patch_level()before it spawns. This patch adds no capability, so bumping it would advertise a tier that does not exist and strand hosts that gate on it. Per the PKGBUILD's own rule this ships as apkgrelbump instead (1 → 2), and the README now spells that split out so the next patch does not have to rediscover it.No packaging file needed editing otherwise: the PKGBUILD, the RPM spec and
packaging/nix/gamescope.nixall globpatches/*.patchrather than enumerating them.Verification
On an NVIDIA host, all six patches
git am-ing onto the pinned upstream commit and then a release build — the shipped configuration, not the debug one used to find this:3.16.25-7-gea635c1+pfhdr4— marker intact2752x2064@120 --steam --xwayland-count 1)Notes for whoever touches this next
Two things cost real time here and are worth knowing:
coredumpctl's inline backtrace is not trustworthy for a stripped binary. For this same crash it rendered four frames insidelibnvidia-eglcorewith alibstdc++/clone3tail, which reads exactly like a worker thread crashing in EGL — a theory I pursued and which was wrong.gdbon the same core against a symbolised build showed the truth. Arch publishes no gamescope debuginfo (404 on debuginfod), so symbolising means building our own with--buildtype=debugoptimized.--xwayland-count 0is an unrelated bug and is left alone here: it dies far earlier, inmain()atwlserver.cpp:3215, dereferencing a nullgamescope_xwayland_server_t. It never reaches teardown. Punktfunk always spawns with--xwayland-count 1, so that path is never taken — but it briefly made the fix look ineffective when the real configuration was already clean.Not filed upstream, per instruction, though the bug is not punktfunk-specific and the patch would apply as-is.