feat(ffi): hand-mirrored C structs are now layout-checked at compile time

The sharpest memory-safety risk left in this codebase is not an `unsafe` block — it is a
hand-written `#[repr(C)]` mirror of an external C struct. Get a field offset wrong and nothing fails
to compile and nothing reliably crashes: the library reads a pointer, a length or a pitch out of the
wrong bytes. Eleven such structs across five files had NO check at all.

Guarded here, each next to the struct it protects:

* `AVCUDADeviceContext`, and `AVD3D11VADeviceContext`/`AVD3D11VAFramesContext`. `ffmpeg-sys-next`
  binds none of them, so these mirrors are the only definitions — and we WRITE through them
  (`cuda_ctx`, `device`, `bind_flags`). ⚠ The D3D11VA pair is duplicated VERBATIM in two crates
  (pf-encode's `ffmpeg_win.rs`, pf-client-core's `video_d3d11.rs`) because neither can depend on the
  other; they must agree with libav and with each other, and now a drift in either is a build error.
* The six cuda.h structs. Three were already asserted — but only in `#[cfg(test)]`, so the check ran
  when someone ran the tests and never in a release build. They are `const` now. The other three,
  including `CUDA_MEMCPY2D` which is filled on EVERY zero-copy frame, had nothing.
* `MsghdrX`, Darwin's `msghdr_x`, which `libc` does not expose. Its layout is not reviewable by eye:
  the 32-bit fields force padding before each following pointer, so `msg_iov` sits at 16 and not 12.
  `sendmsg_x`/`recvmsg_x` take the pointer and length from it.
* `IPolicyConfigVtbl` — the sharpest of the set. It mirrors an UNDOCUMENTED COM interface, and
  `set_default_endpoint` is called by SLOT INDEX through a ten-entry `_reserved` gap that carries no
  names to anchor a review. A field added or resized above it does not break the build; it calls a
  different function pointer through a mismatched signature.

Every assertion is `const _: () = assert!(..)`, so it holds on every build including release and
cannot be skipped. The compiler verified the numbers — the sizes and offsets asserted here are the
ones the target actually produces, on each platform that compiles the struct.

Verified: Linux .21 fmt + both CI clippy steps rc=0 (CUDA + libav CUDA mirrors); Windows .47 full CI
clippy set rc=0 + pf-capture tests (D3D11VA pair, COM vtable); macOS `cargo check -p punktfunk-core`
(MsghdrX — the only platform that compiles it).
This commit is contained in:
2026-07-29 08:48:42 +02:00
parent 8a5a5edc37
commit aa070f2d55
7 changed files with 159 additions and 27 deletions
+26
View File
@@ -149,6 +149,32 @@ struct AVD3D11VAFramesContext {
texture_infos: *mut c_void, // AVD3D11FrameDescriptor* (FFmpeg-managed)
}
// Hand-written mirrors of libav's `AVD3D11VADeviceContext` / `AVD3D11VAFramesContext`
// (hwcontext_d3d11va.h) — `ffmpeg-sys-next` binds neither, and we WRITE `device` / `bind_flags`
// through them, so a wrong offset is silent corruption of libav's context rather than a compile
// error. ⚠ These two structs are duplicated in the other crate that talks to the same libav
// contexts (pf-encode's `ffmpeg_win.rs` and pf-client-core's `video_d3d11.rs`); they must agree
// with libav AND with each other, and these assertions are what makes a drift in either a build
// failure instead of a runtime mystery.
const _: () = {
use std::mem::{offset_of, size_of};
type P = *mut c_void;
assert!(size_of::<AVD3D11VADeviceContext>() == 7 * size_of::<P>());
assert!(offset_of!(AVD3D11VADeviceContext, device) == 0);
assert!(offset_of!(AVD3D11VADeviceContext, device_context) == size_of::<P>());
assert!(offset_of!(AVD3D11VADeviceContext, video_device) == 2 * size_of::<P>());
assert!(offset_of!(AVD3D11VADeviceContext, video_context) == 3 * size_of::<P>());
assert!(offset_of!(AVD3D11VADeviceContext, lock) == 4 * size_of::<P>());
assert!(offset_of!(AVD3D11VADeviceContext, unlock) == 5 * size_of::<P>());
assert!(offset_of!(AVD3D11VADeviceContext, lock_ctx) == 6 * size_of::<P>());
// ptr, u32, u32, ptr — the two 32-bit flags pack into one pointer-sized slot with no padding.
assert!(size_of::<AVD3D11VAFramesContext>() == 3 * size_of::<P>());
assert!(offset_of!(AVD3D11VAFramesContext, texture) == 0);
assert!(offset_of!(AVD3D11VAFramesContext, bind_flags) == size_of::<P>());
assert!(offset_of!(AVD3D11VAFramesContext, misc_flags) == size_of::<P>() + 4);
assert!(offset_of!(AVD3D11VAFramesContext, texture_infos) == 2 * size_of::<P>());
};
fn averr(what: &str, code: i32) -> anyhow::Error {
anyhow!("{what}: {}", ffmpeg::Error::from(code))
}