fix(gamescope): every session ended in a SIGSEGV at exit
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.
This commit is contained in:
@@ -20,7 +20,9 @@ pkgname=punktfunk-gamescope
|
||||
_gsver=3.16.25
|
||||
_gsrev=8c676c399c761e4540587f61004c957993d12fea
|
||||
pkgver="${_gsver}.pfhdr4"
|
||||
pkgrel=1
|
||||
# 2: patch 0006 (never destroy the Vulkan device/output at exit). No capability moved, so the
|
||||
# `.pfhdrN` level deliberately stays put — see README.md.
|
||||
pkgrel=2
|
||||
pkgdesc="gamescope with 10-bit BT.2020/PQ PipeWire capture, for punktfunk HDR streaming"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://git.unom.io/unom/punktfunk"
|
||||
|
||||
@@ -16,6 +16,7 @@ The patches here add the missing half, and nothing else. See
|
||||
| `0003-headless-advertise-the-virtual-display-s-mode-and-re.patch` | Give `CHeadlessConnector` a real `GetModes()` + `GetValidDynamicRefreshRates()` from the resolved `-W`/`-H`/`-r`, report `GAMESCOPE_SCREEN_TYPE_EXTERNAL` so `update_mode_atoms` publishes the list, and add `--custom-refresh-rates` | **Yes** — a headless session that cannot report its own mode is a plain bug |
|
||||
| `0004-pipewire-optionally-composite-the-external-overlay-i.patch` | `--pipewire-composite-external-overlay` (off by default): paint the external overlay layer (mangoapp — the fps/stats readout) into the capture stream | **Yes** — same shape as the cursor patch, same argument |
|
||||
| `0005-punktfunk-stamp-the-version-banner-with-pfhdrN.patch` | Append `+pfhdr<N>` to the `--version` banner | **No** — ours only, retired when the functional patches above land upstream |
|
||||
| `0006-punktfunk-never-destroy-the-Vulkan-device-or-output-.patch` | Give `g_device` and `g_output` storage that is never destroyed, so their destructors cannot call a Vulkan driver glibc has already unloaded at `exit()` | **Yes** — a plain static-destruction-order bug, not punktfunk-specific |
|
||||
|
||||
### Why the headless patch matters
|
||||
|
||||
@@ -59,6 +60,12 @@ The number is a **monotonic patch-set revision**, so one probe answers every cap
|
||||
|
||||
Bump it whenever a patch adds or changes something the host must know about before it spawns.
|
||||
|
||||
A patch that only fixes a crash does **not** bump it: `0006` (the exit-time Vulkan teardown fix)
|
||||
changes nothing the host probes for, so the level stays `+pfhdr4` and the rebuild ships as a
|
||||
`pkgrel` bump instead — exactly the split the PKGBUILD's own comment describes. Bumping the level
|
||||
for a bugfix would be worse than useless: it would advertise a capability tier that does not exist
|
||||
and strand hosts that gate on it.
|
||||
|
||||
⚠️ The two indirect spawn modes (the `GAMESCOPE_BIN` wrapper for gamescope-session-plus, and the
|
||||
SteamOS PATH shim) pass these flags through `PF_HDR_ARGS`, so they share one dependency: if the
|
||||
session ignores `GAMESCOPE_BIN`/`PATH` and execs the distro's gamescope, it gets neither the HDR
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
From 509fb928c7dc3307372629ca692f4c895c4fe984 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Enrico=20B=C3=BChler?= <enrico.buehler@unom.io>
|
||||
Date: Sat, 8 Aug 2026 19:17:25 +0200
|
||||
Subject: [PATCH] punktfunk: never destroy the Vulkan device or output at exit
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Every gamescope session punktfunk spawns ended in SIGSEGV. It happened after
|
||||
the compositor had already done its work — "Primary child shut down!", then a
|
||||
coredump — so the stream itself looked fine and the crash only showed up as a
|
||||
steady drip of coredumps and a non-zero exit from the spawn.
|
||||
|
||||
The cause is static destruction order, 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 equals the instruction pointer, which is the signature of exactly that:
|
||||
|
||||
#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()
|
||||
|
||||
On NVIDIA it is 100% reproducible:
|
||||
`gamescope --backend headless -W 1280 -H 720 -r 60 --xwayland-count 1 -- true`
|
||||
exits 139 every time, and cleanly with this patch (5/5, plus 2/2 at the real
|
||||
session's 2752x2064@120 --steam).
|
||||
|
||||
Nothing needs freeing at that point. The process is exiting; the kernel
|
||||
reclaims the device, its command buffers and every GPU allocation. So give both
|
||||
objects 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. `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 helper and not a one-off.
|
||||
---
|
||||
src/rendervulkan.cpp | 35 +++++++++++++++++++++++++++++++++--
|
||||
src/rendervulkan.hpp | 4 ++--
|
||||
2 files changed, 35 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp
|
||||
index 5c2dd11..8cd5ca2 100644
|
||||
--- a/src/rendervulkan.cpp
|
||||
+++ b/src/rendervulkan.cpp
|
||||
@@ -118,7 +118,37 @@ static VkResult vulkan_load_module()
|
||||
return s_result;
|
||||
}
|
||||
|
||||
-VulkanOutput_t g_output;
|
||||
+// punktfunk: globals that own GPU objects must OUTLIVE static destruction.
|
||||
+//
|
||||
+// gamescope kept its Vulkan device and output as plain globals, so glibc ran their destructors
|
||||
+// from `__run_exit_handlers` after main() returned. Those destructors call back into the driver
|
||||
+// (~CVulkanCmdBuffer -> vk.FreeCommandBuffers, ~CVulkanTexture -> vk.Destroy*), but by then the
|
||||
+// Vulkan ICD has already been torn down and unloaded, so the call jumps through a function
|
||||
+// pointer into an unmapped page: SIGSEGV at exactly the address it tried to execute. On NVIDIA it
|
||||
+// is 100% reproducible -- `gamescope --backend headless ... -- true` dies with exit 139 EVERY
|
||||
+// time -- so every punktfunk gamescope session ended in a coredump.
|
||||
+//
|
||||
+// Nothing needs freeing at that point: the process is exiting and the kernel reclaims the device,
|
||||
+// its command buffers and every GPU allocation. So give these objects storage that is constructed
|
||||
+// exactly as before but NEVER destroyed. A union member is only destroyed if the union says so,
|
||||
+// and ours deliberately does not.
|
||||
+//
|
||||
+// Fixing only one of them just moves the crash to the next global (verified: pinning the device
|
||||
+// relocated the fault into ~VulkanOutput_t), which is why this is a shared helper rather than a
|
||||
+// one-off.
|
||||
+namespace
|
||||
+{
|
||||
+ template <typename T>
|
||||
+ union CNoDestroy
|
||||
+ {
|
||||
+ T value;
|
||||
+ CNoDestroy() : value() {}
|
||||
+ ~CNoDestroy() {} // deliberately does NOT destroy `value`
|
||||
+ };
|
||||
+}
|
||||
+
|
||||
+namespace { CNoDestroy<VulkanOutput_t> g_outputHolder; }
|
||||
+VulkanOutput_t &g_output = g_outputHolder.value;
|
||||
|
||||
uint32_t g_uCompositeDebug = 0u;
|
||||
gamescope::ConVar<uint32_t> cv_composite_debug{ "composite_debug", 0, "Debug composition flags" };
|
||||
@@ -1943,7 +1973,8 @@ void CVulkanCmdBuffer::insertBarrier(bool flush)
|
||||
0, 0, nullptr, 0, nullptr, barriers.size(), barriers.data());
|
||||
}
|
||||
|
||||
-CVulkanDevice g_device;
|
||||
+namespace { CNoDestroy<CVulkanDevice> g_deviceHolder; }
|
||||
+CVulkanDevice &g_device = g_deviceHolder.value;
|
||||
|
||||
static bool allDMABUFsEqual( wlr_dmabuf_attributes *pDMA )
|
||||
{
|
||||
diff --git a/src/rendervulkan.hpp b/src/rendervulkan.hpp
|
||||
index b6749d4..a9335c4 100644
|
||||
--- a/src/rendervulkan.hpp
|
||||
+++ b/src/rendervulkan.hpp
|
||||
@@ -564,7 +564,7 @@ enum ShaderType {
|
||||
SHADER_TYPE_COUNT
|
||||
};
|
||||
|
||||
-extern VulkanOutput_t g_output;
|
||||
+extern VulkanOutput_t &g_output;
|
||||
|
||||
struct SamplerState
|
||||
{
|
||||
@@ -1007,4 +1007,4 @@ void vulkan_wait_idle();
|
||||
// Whether the driver implements VK_EXT_physical_device_drm
|
||||
bool vulkan_has_drm_props();
|
||||
|
||||
-extern CVulkanDevice g_device;
|
||||
+extern CVulkanDevice &g_device;
|
||||
--
|
||||
2.55.0
|
||||
|
||||
Reference in New Issue
Block a user