Files
punktfunk/design/zerocopy-hardening-handoff.md
T
enricobuehler 8ef320662b docs: zero-copy EGL/CUDA capture-crash hardening handoff
Describes a reproduced host SIGSEGV in cuGraphicsMapResources (inside
libnvidia-eglcore), reached via zerocopy::cuda / zerocopy::egl on the tiled
EGL/GL->CUDA capture path, when the KWin dmabuf is invalidated mid-map (observed
on .181 during a Game->Desktop switch under zero-copy, with the compositor itself
crashing). Pre-existing capture-layer issue, not the gamemode work. Issue
description + root cause + solution-space considerations only -- the next agent
plans the implementation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 06:26:22 +00:00

189 lines
12 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: HANDOFF — issue description only (2026-07-06).** 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).