Files
punktfunk/design/zerocopy-hardening-handoff.md
T
enricobuehler 077d8f85ca feat(host): isolate the zero-copy GPU import in a worker process
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>
2026-07-06 10:33:43 +00:00

198 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](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 a `kill -9` of 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`/`RegisteredTexture` vs. 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 from
`capture/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 `RegisteredTexture` outlives 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.
- **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_process`
callback 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 `cuGraphicsMapResources` fault 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}.rs` are part of the unsafe-audited
set (`#![deny(clippy::undocumented_unsafe_blocks)]`, every `unsafe` carries a `// SAFETY:`). Any fix
updates those proofs to reflect the new lifetime/validity guarantees.
## 10. Reproduction environment / artifacts
- Box: `bazzite@192.168.1.181` (sudo `bazzite`), 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 `coredumpctl` on 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_ZEROCOPY` semantics (ON for VAAPI/AMD/Intel with a CPU downgrade;
OFF/opt-in for NVENC).