The tiled EGL/GL→CUDA import crashed the whole host (SIGSEGV inside libnvidia-eglcore via cuGraphicsMapResources) when the compositor invalidated an imported dmabuf mid-map — reproduced on the Bazzite F44 Game→Desktop switch (design/zerocopy-hardening-handoff.md). A driver SIGSEGV is uncatchable in-process, so the whole EglImporter (tiled EGL/GL→CUDA and LINEAR Vulkan→CUDA) now runs in a per-capture `zerocopy-worker` subprocess: dmabuf fds go over a SEQPACKET socketpair (SCM_RIGHTS, sent once per buffer keyed by dmabuf st_ino; NeedFd resend self-heals cache desync), frames come back as CUDA-IPC pooled device buffers (still zero-copy, +one socket RTT/frame). Worker death poisons the capturer so the existing capture-loss rebuild runs — the host survives; 3 consecutive deaths latch the GPU import off (CPU/SHM path). PUNKTFUNK_ZEROCOPY_INPROC=1 keeps the old in-process import for debugging/A-B. Also fixed along the way: a failed *tiled* import no longer falls through to the CPU mmap de-pad (which scrambled tiled bytes; LINEAR keeps the fallback); Nv12Blit dropped its GL textures while still CUDA-registered (unregister now runs first); GlBlit had no Drop at all (GL objects leaked per size change); VkBridge's per-fd src cache is now invalidated on renegotiation/eviction instead of never. Design: design/zerocopy-worker-isolation.md. Unit tests: 14 new (protocol fd-passing, worker dispatch, client handshake/death/NeedFd, death latch). On-glass validated on the RTX 5070 Ti/GNOME box (.21): the worker path streams at p50 1.30 ms (NV12, 1800 frames 0-mismatched, parity with the in-process path), and a kill -9 of the worker mid-stream is survived by the host and recovered — poison -> capture lost, rebuilding pipeline in place -> a fresh worker in ~185 ms -> streaming resumes (2385 frames, 0 mismatched). A real KWin compositor-crash repro is still pending (a worker kill -9 is strictly harsher, so it corroborates). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
13 KiB
Zero-copy capture hardening — issue handoff
Status: FIXED + validated (2026-07-06). The fix is implemented and on-glass validated — see
zerocopy-worker-isolation.md: the GPU import (tiled EGL/GL→CUDA and LINEAR Vulkan→CUDA) now runs in a per-capture worker subprocess (CUDA-IPC frame hand-off), so this driver SIGSEGV kills the worker and the host degrades to its capture-loss rebuild; plus in-process teardown-order fixes and a poison/latch path replacing the corrupt tiled→CPU fallback. Validated on the RTX 5070 Ti/GNOME box: worker path streams at p50 1.30 ms, and akill -9of the worker mid-stream is survived + recovered (fresh worker in ~185 ms, streaming resumes). The description below is kept as the issue record.(Original handoff intro:) This document describes a reproduced host SIGSEGV in the Linux zero-copy capture path. It deliberately does not prescribe a fix — the next agent plans the implementation. Everything below is observed fact + root-cause analysis; the "Considerations / open questions" section frames the solution space without committing to one.
This is a pre-existing capture-layer issue, NOT a regression from the gamemode/dedicated-sessions work (
design/gamemode-and-dedicated-sessions.md). The crashing code (crates/punktfunk-host/src/linux/zerocopy/{cuda,egl}.rs,capture/linux/pipewire.rs) is untouched by that branch; it merely surfaced during on-glass validation of it.
1. What happened
On .181 (Bazzite F44, RTX 4090, NVIDIA open driver 610.43.02 — see
[[gamemode-onglass-181-2026-07-06]]), streaming a game at the client's mode worked smoothly with
zero-copy enabled (PUNKTFUNK_ZEROCOPY=1). When the user then switched the box from Steam Game
Mode to the KDE/Plasma desktop mid-stream, the host process crashed (SIGSEGV, core dumped) and
the client saw "session ended". systemd auto-restarted the host ~3 s later.
The host's own logic on the switch was correct up to the crash — the session watcher detected
Gaming → DesktopKde, bumped the session epoch, rebuilt the backend to KWin, brought up the virtual
output at 5120×1440@240, set it sole desktop, and began PipeWire capture. The crash came after
that, the first time the capture thread mapped a KWin frame into CUDA.
2. The crash (backtrace)
coredumpctl for the host process, crashing thread:
Signal: 11 (SEGV)
#0 libnvidia-eglcore.so.610.43.02 ← SIGSEGV inside the NVIDIA driver
#1 libnvidia-eglcore.so.610.43.02
#2 libnvidia-eglcore.so.610.43.02
#3 libnvidia-eglcore.so.610.43.02
#4 libEGL_nvidia.so.0
#5 libcuda.so.1
#6 libcuda.so.1
#7 cuGraphicsMapResources (libcuda.so.1)
#8 punktfunk_host::zerocopy::cuda::RegisteredTexture::copy_mapped_plane
#9 punktfunk_host::zerocopy::egl::EglImporter::import_inner
#10 punktfunk_host::capture::linux::pipewire::pipewire_thread::{{closure}} (the PipeWire on_process callback)
#11 pipewire::stream::…::on_process
#12 libpipewire-0.3.so.0 (impl_node_process_input → process_node → node_on_fd_events → pw_main_loop_run)
The fault is inside libnvidia-eglcore, reached through cuGraphicsMapResources — i.e. CUDA was
asked to map a GL/EGL-imported resource, and the driver dereferenced GPU state that was no longer
valid.
3. The code path
This is the tiled-dmabuf zero-copy path used for compositors that hand out tiled buffers
(KWin, and the NVIDIA tiled path generally): PipeWire dmabuf → EGL/GL import (EglImporter,
crates/punktfunk-host/src/linux/zerocopy/egl.rs) → register as a CUDA graphics resource and
map per frame (RegisteredTexture::copy_mapped_plane / copy_mapped_to,
crates/punktfunk-host/src/linux/zerocopy/cuda.rs:810-848 and the sibling copy_mapped_plane below
it). The crash is at the cuGraphicsMapResources(1, &mut self.resource, …) call
(cuda.rs:824-828), driven per frame from the PipeWire on_process callback
(capture/linux/pipewire.rs, the closure at backtrace frame #10).
The relevant // SAFETY: proof (cuda.rs:814-823) reasons that self.resource is a valid
CUgraphicsResource from a successful register_gl, the GL+CUDA contexts are current, and the
map/unmap pair is balanced with the copy synced before unmap. That reasoning is sound for a
well-behaved compositor that keeps the imported dmabuf alive across the map — it does not cover the
case where the producer invalidates the underlying buffer/texture out from under an in-flight
map.
Not affected on this box: the gamescope path (LINEAR dmabuf → Vulkan bridge → CUDA,
linux/zerocopy/vulkan.rs) did not crash — game-mode streaming was smooth. And the non-zero-copy
SHM/CPU path (PUNKTFUNK_ZEROCOPY=0, the NVENC default) has no EGL/CUDA import at all, so there is
nothing for the driver to fault on. So the fault is specific to the EGL/GL→CUDA tiled import, not
zero-copy in general.
4. Root cause
cuGraphicsMapResources faulted inside the driver on GPU state that had become invalid — almost
certainly because the KWin compositor freed/recycled (or crashed with) the dmabuf that our
EglImporter had imported and registered, while our capture thread was mapping it for the next frame.
Strong corroborating evidence: in the same 8-second window, the box logged core dumps for
plasmashell (×2), Xwayland, gamescope, and mangoapp. So the F44 Game↔Desktop transition on
this box is itself highly unstable, and the KWin buffer our zero-copy path held a handle to almost
certainly went away mid-map.
Why this is a real host-robustness gap, not just "the box is flaky": a SIGSEGV inside a
closed-source driver (libnvidia-eglcore) is not catchable from Rust — it is not a Result, not a
Rust panic, not something a catch_unwind can contain. So any time the producer's buffer can vanish
between "we hold a CUDA graphics resource for it" and "we map it" (compositor crash, buffer-pool
recycle, output/mode teardown, hot-unplug), the driver can take the whole host process down. A capture
pipeline that must survive a compositor going away (which the host already tries to do — it has a
capture-loss → rebuild path) cannot rely on cuGraphicsMapResources returning an error on a stale
resource; the driver may just crash instead.
5. Trigger conditions (what invalidates the imported buffer)
The observed trigger was a compositor crash during a Game→Desktop switch, but the same class of
fault can be reached by anything that frees/recycles the imported dmabuf or its GL texture while a map
is in flight or a RegisteredTexture still references it:
- compositor crash / restart (observed);
- normal PipeWire buffer-pool recycle / renegotiation (format change, buffer count change) where a registered texture outlives the buffer it wrapped;
- virtual-output teardown / mode change (e.g. the mid-stream
Reconfigure, the session-switch rebuild) racing an in-flight map; - output removal / disconnect.
The next agent should establish which of these are actually reachable in the current code (the
per-frame registration/lifetime in
EglImporter/RegisteredTexturevs. the PipeWire buffer lifecycle) versus only the compositor-crash case.
6. Scope
- Affected: the EGL/GL→CUDA tiled-import path (
zerocopy::egl+zerocopy::cuda), driven fromcapture/linux/pipewire.rs. On NVIDIA this is used for tiled dmabufs (KWin desktop capture is the concrete case here; the NVIDIA tiled path in general). - Likely also worth auditing (same class): the Vulkan bridge path (
zerocopy::vulkan.rs, LINEAR dmabuf → Vulkan → CUDA) — it did not crash here, but it imports external dmabufs into the GPU too and may have the same "producer freed the buffer mid-use" exposure; confirm whether it validates/owns the buffer lifetime differently. - Not affected: the SHM/CPU capture path (no GPU import).
7. What is NOT the cause (to save the next agent time)
- Not the gamemode/dedicated-sessions branch. That branch's switch logic worked correctly (epoch bump, watcher rebuild to KWin, virtual output up); the crash is downstream in pre-existing capture code it doesn't touch.
- Not a
.desktop/KWin authorization problem. The KWin virtual output was created and set sole desktop successfully — auth was fine. - Not the gamescope "out of buffers" issue from the same validation session (that was a separate gamescope-3.16.19 PipeWire-node limitation on SHM). This is a hard driver SEGV on the GPU-import path.
8. Observed mitigation (already available, not the fix)
Setting PUNKTFUNK_ZEROCOPY=0 (SHM/CPU path — the NVENC default anyway; the box had it forced
=1) removes the EGL/CUDA import, so this crash cannot occur and a compositor going away degrades to a
graceful capture-loss rebuild. Cost: a CPU copy per frame (higher latency than the zero-copy stream the
user measured). This is an operational workaround, not a code fix, and it forfeits zero-copy on the
desktop path.
9. Considerations / open questions for the implementation plan (do not treat as a prescription)
These frame the solution space; the next agent decides.
- A driver SIGSEGV is uncatchable in-process. Any design that "handles" this by wrapping the FFI
call in error handling will not work — the process is already dead. So the fix has to be about
never handing the driver a resource that can be stale, or isolating the GPU-import work so a
driver crash doesn't take the streaming host down. Both directions are open:
- Prevent-the-stale-resource directions to evaluate: strict per-frame import/register/map/unmap
lifetime tied to the exact PipeWire buffer being processed (so no
RegisteredTextureoutlives its buffer); detecting compositor/output teardown and stopping capture before the next map; reconciling the EGL texture / CUDA resource lifetime with PipeWire's buffer-recycle events. Establish whether the current code can ever map a resource whose buffer PipeWire has already recycled/removed. - Isolate-the-crash directions to evaluate: whether the GPU import belongs in a separate process (like the Windows two-process/DDA isolation model) so a driver SEGV is contained and the session can rebuild — heavier, but the only thing that truly survives an unpreventable driver fault.
- Prevent-the-stale-resource directions to evaluate: strict per-frame import/register/map/unmap
lifetime tied to the exact PipeWire buffer being processed (so no
- Per-backend / per-buffer-type routing. The Vulkan-bridge path did not crash; the SHM path is safe. A plan might route tiled dmabufs (KWin) away from the fragile EGL/CUDA path, or only enable the EGL/CUDA path where the producer's buffer lifetime is guaranteed. Decide whether the fix is "harden the EGL/CUDA path" vs. "don't use it for producers that can pull buffers."
- Interaction with the existing capture-loss rebuild. The host already rebuilds on capture loss;
the goal is to reach that path on producer teardown instead of the driver crash. Understand why
the crash beats the capture-loss detection today (the map happens in the PipeWire
on_processcallback before any loss is observed). - Reproducibility. This was observed on a box whose KDE was itself crashing (F44 plasmashell/
Xwayland). To isolate "our zero-copy path is fragile" from "the compositor crashed," reproduce on a
stable KDE/NVIDIA box — force a buffer invalidation (output teardown / renegotiation / a scripted
compositor restart) mid-capture and confirm the same
cuGraphicsMapResourcesfault without the surrounding compositor chaos. That also tells you whether the non-crash triggers in §5 are real. - Keep the SAFETY-proof discipline.
zerocopy/{cuda,egl,vulkan}.rsare part of the unsafe-audited set (#![deny(clippy::undocumented_unsafe_blocks)], everyunsafecarries a// SAFETY:). Any fix updates those proofs to reflect the new lifetime/validity guarantees.
10. Reproduction environment / artifacts
- Box:
bazzite@192.168.1.181(sudobazzite), Bazzite F44 (bazzite-deck-nvidia:testing), RTX 4090, NVIDIA open 610.43.02. See[[gamemode-onglass-181-2026-07-06]]for deploy/access details. - Trigger: stream a game-mode session with
PUNKTFUNK_ZEROCOPY=1, then switch the box Game Mode → KDE desktop mid-stream (the session watcher rebuilds to KWin → tiled EGL/CUDA capture → crash). - The coredump was present under
coredumpctlon the box at the time of writing (may age out); the backtrace in §2 is captured above.
11. Related
design/gamemode-and-dedicated-sessions.md(the branch this surfaced under — not the cause).design/session-aware-host-followups.md(Game↔Desktop switch behavior; F44 GPU instability #1).design/gpu-contention-investigation.md/design/host-latency-plan.md(zero-copy path context).crates/punktfunk-host/src/linux/zerocopy/{egl,cuda,vulkan}.rs,capture/linux/pipewire.rs.- CLAUDE.md: "GPU zero-copy on all paths (tiled dmabuf → EGL/GL → CUDA; LINEAR dmabuf → Vulkan
bridge → CUDA)" and the
PUNKTFUNK_ZEROCOPYsemantics (ON for VAAPI/AMD/Intel with a CPU downgrade; OFF/opt-in for NVENC).