fix(zerocopy): error-path leaks, exception-safe fence wait, odd-height NV12 overrun, worker reaper deadline

Seven medium findings from the round-1 sweep, all in pf-zerocopy. Adjudicated
against source; all zero-copy-preserving (no GPU→CPU→GPU roundtrip introduced).

- import_src (vulkan.rs) leaked a VkBuffer + VkDeviceMemory + dup'd fd on every
  fallible step before the success-only src_cache.insert. A failed import is
  survived by the worker and RETRIED by the caller every frame, so this leaked
  per frame for the worker's whole lifetime. Now owns the dup fd via OwnedFd
  (closes on early return until Vulkan consumes it at allocate_memory) and
  destroys the buffer + frees memory on each error path.

- ensure_dst destroyed the old exportable buffer and nulled self.dst BEFORE
  building the replacement, so a failed rebuild both dropped the working buffer
  and leaked every object the partial rebuild created (raw ash handles, no Drop;
  VkBridge::drop only frees the live self.dst). Now builds fully, unwinds each
  error locally, and swaps only on success.

- The submit→wait→reset sequence in import_linear_nv12 / import_linear `?`-ed out
  of wait_for_fences BEFORE reset_fences on a TIMEOUT/DEVICE_LOST, leaving the
  shared self.cmd PENDING and self.fence IN-USE while the caller retries on the
  same bridge (and ensure_dst later destroys dst.buffer assuming nothing is in
  flight). Now drains the GPU (device_wait_idle) and resets the fence before
  propagating — fixing the reported ensure_dst UAF at its source.

- copy_pitched_nv12_to_buffer writes height.div_ceil(2) chroma rows into a UV
  plane sized at height/2, so an odd height overruns by one uv_pitch row (OOB
  device write / CUDA_ERROR_ILLEGAL_ADDRESS). Added the even-dimension guard to
  import_linear_nv12, matching Nv12Blit/Yuv444Blit.

- sweep_reaper only reaped already-exited workers, so a worker wedged inside a
  driver call lingered forever holding its CUcontext + BufferPool (hundreds of MB
  VRAM). Now force-kills a child parked past REAPER_KILL_DEADLINE (20s).

Compile + tests green on Linux .21 (RTX 5070 Ti): pf-zerocopy 17/0. The error
paths themselves are not fault-injected; the fixes are structural.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 23:10:06 +02:00
parent 986402f731
commit 26ff005e70
3 changed files with 160 additions and 66 deletions
+20 -4
View File
@@ -21,7 +21,7 @@ use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use std::time::{Duration, Instant};
/// Handshake budget: EGL + CUDA bring-up is ~200 ms; a cold driver load can take seconds.
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(20);
@@ -64,11 +64,27 @@ impl Drop for Shared {
/// Children whose worker hasn't exited yet at `RemoteImporter` drop time (it exits on socket
/// EOF, i.e. after the last in-flight frame drops). Swept on every spawn and every drop so
/// workers don't linger as zombies for more than one capture generation.
static REAPER: Mutex<Vec<Child>> = Mutex::new(Vec::new());
static REAPER: Mutex<Vec<(Child, Instant)>> = Mutex::new(Vec::new());
/// How long past `REPLY_TIMEOUT` a parked worker may linger before it is force-killed. A worker
/// wedged INSIDE a driver call never observes socket EOF, so `try_wait` alone would keep it (and
/// its CUcontext + BufferPool — order hundreds of MB of VRAM) forever.
const REAPER_KILL_DEADLINE: Duration = Duration::from_secs(20);
fn sweep_reaper() {
let mut list = REAPER.lock().unwrap();
list.retain_mut(|c| !matches!(c.try_wait(), Ok(Some(_))));
let now = Instant::now();
list.retain_mut(|(c, parked)| {
if matches!(c.try_wait(), Ok(Some(_))) {
return false; // exited on its own → reaped
}
if now.duration_since(*parked) > REAPER_KILL_DEADLINE {
let _ = c.kill();
let _ = c.wait();
return false; // wedged past the deadline → force-killed + reaped
}
true
});
}
/// Fd pinned to this process's own executable image, opened (once, lazily) via the
@@ -455,7 +471,7 @@ impl Drop for RemoteImporter {
// gone; park the rest for the next sweep.
if let Some(mut child) = self.child.take() {
if !matches!(child.try_wait(), Ok(Some(_))) {
REAPER.lock().unwrap().push(child);
REAPER.lock().unwrap().push((child, Instant::now()));
}
}
sweep_reaper();