docs(avhw): fix misleading Send soundness reasoning
CI / Build + Clippy + Test (pull_request) Failing after 30s
CI / Security audit (RUSTSEC) (pull_request) Failing after 30s

Oracle audit of all 5 `unsafe impl Send` in src/avhw/ found soundness
intact but reasoning wrong in 3 of 5:

- AvHwDevCtx: claimed '&mut self ensures exclusive access' — false,
  ref_clone() hands raw pointers to other threads / FFmpeg-internal
  codec workers. Real basis is AVBufferRef atomic_uint refcount +
  libva VADisplay thread safety.
- AvHwFrameCtx: claimed 'send/receive pattern is thread-safe' —
  misdirection. Real basis is AVBufferPool atomic get/put.
- EncState: claimed 'raw pointers not shared across threads' — false
  when FFmpeg frame/slice threading is enabled. Real basis is the
  hw device/frames contexts being designed for such sharing.

SwEncState and SwEncEncode comments were acceptable; improved for
clarity (note that contained FFmpeg handles are non-thread-safe but
Send-sound under exclusive access, and that crossbeam/Arc fields are
already Send by design).

Added module-level convention doc to src/avhw/mod.rs centralizing
the C-API-level justification rule and explicitly calling out the
'&mut self as Send basis' anti-pattern so future contributors don't
repeat the category error.

Fixed AGENTS.md:
- Stale claim that Cargo.toml 'only warns' on undocumented_unsafe_blocks
  (it's been 'deny' for a while)
- Stale path src/avhw.rs → src/avhw/ (split in d53e881)
- Stale 'avoid moving wrappers across threads' guidance — Send is sound,
  the audit just confirmed why

No code behavior change. Verified: cargo build --release,
cargo clippy --release --all-targets (0 warnings),
cargo test --release (79 unit + 3 integration, 1 ignored).
This commit is contained in:
2026-07-09 15:20:16 +08:00
parent 9a7b745a0e
commit fed8c2dcfd
6 changed files with 65 additions and 20 deletions
+16 -6
View File
@@ -12,9 +12,15 @@ pub struct AvHwDevCtx {
ptr: *mut ffi::AVBufferRef,
}
// SAFETY: AvHwDevCtx wraps an FFmpeg AVBufferRef which is not Send by default,
// but we guarantee exclusive access through &mut self. The underlying VAAPI
// device context is thread-safe for the operations we perform.
// SAFETY: AVBufferRef's refcount is atomic (atomic_uint in libavutil/buffer.c);
// av_buffer_ref / av_buffer_unref are safe to call concurrently from different
// threads on the same buffer. The underlying AVHWDeviceContext (VAAPI VADisplay)
// is designed by FFmpeg/libva to be shared across codec and filter contexts,
// including across FFmpeg-internal codec threads. Raw refs returned by
// ref_clone() may outlive this wrapper and be consumed by other threads; this
// is the intended usage pattern and is sound because refcount management is
// atomic. The &mut self on Rust methods is an API convenience, not the basis
// for soundness.
unsafe impl Send for AvHwDevCtx {}
impl AvHwDevCtx {
@@ -65,9 +71,13 @@ pub struct AvHwFrameCtx {
ptr: *mut ffi::AVBufferRef,
}
// SAFETY: AvHwFrameCtx wraps an FFmpeg AVBufferRef to an AVHWFramesContext.
// It is only accessed through &mut self, ensuring no concurrent mutation.
// The underlying hardware frames pool is thread-safe for the send/receive pattern.
// SAFETY: AVBufferRef's refcount is atomic (see AvHwDevCtx). The underlying
// AVHWFramesContext allocates from an AVBufferPool, whose get/put operations
// are atomic and thread-safe. av_hwframe_get_buffer and av_hwframe_transfer_data
// are safe to call concurrently on distinct AVFrames. Cloned refs are typically
// attached to AVCodecContext.hw_frames_ctx and accessed by FFmpeg-internal codec
// threads; this is the designed usage. The &mut self on Rust methods is not the
// basis for soundness.
unsafe impl Send for AvHwFrameCtx {}
impl AvHwFrameCtx {
+5 -2
View File
@@ -53,8 +53,11 @@ pub struct SwEncEncode {
/// MP4 mode keeps 1/fps time_base for file output simplicity.
pub const WEBRTC_RTP_CLOCK_HZ: i128 = 90_000;
// SAFETY: SwEncEncode owns sws_ctx/yuv_frame/enc_video exclusively after construction.
// It is moved to a single encode thread and only accessed through &mut self there.
// SAFETY: SwEncEncode is moved to a single encode thread and accessed only there
// via &mut self. SwsContext, AVFrame, and AVCodecContext are NOT thread-safe for
// concurrent access but are Send-sound under single-thread exclusive use, which
// the encode worker invariant provides. crossbeam Receiver and Arc<AtomicBool>
// are Send by design.
unsafe impl Send for SwEncEncode {}
impl SwEncEncode {
+9 -8
View File
@@ -27,14 +27,15 @@ pub struct EncState {
frames_written: bool,
}
// SAFETY: EncState is moved to exactly one thread (the encode worker) and used
// exclusively there. All fields are either plain Copy types (Option<i64>, bool)
// or ffmpeg-next / AvHw* owned wrappers whose raw inner pointers are not actually
// shared across threads - they're touched only from the owning encode thread.
// This impl exists only to satisfy Rust's auto-Send inference (which can't see
// through the raw pointers hidden inside the wrappers). Do NOT add fields that
// introduce shared mutable state without re-auditing this assumption; see
// AGENTS.md "Unsafe and FFI work" for the documented exclusivity requirement.
// SAFETY: EncState is moved to exactly one encode worker thread and all Rust
// methods take &mut self, so there is no concurrent *Rust-side* access.
// FFmpeg-internal codec threads may touch hw_device_ctx / frames_rgb through
// the encoder context if frame/slice threading is enabled; this is sound
// because AVHWDeviceContext and AVHWFramesContext are designed for such
// sharing (atomic refcounts, thread-safe pool, libva VADisplay thread safety).
// This impl only lifts auto-Send inference through raw pointers inside the
// ffmpeg-next wrappers; it does not introduce new sharing. Do NOT add fields
// that create shared mutable state across threads without re-auditing.
unsafe impl Send for EncState {}
impl EncState {
+29
View File
@@ -1,3 +1,32 @@
//! FFmpeg / VAAPI encoder wrappers.
//!
//! ## `Send` justification convention
//!
//! Several types in this module (`AvHwDevCtx`, `AvHwFrameCtx`, `EncState`,
//! `SwEncState`, `SwEncEncode`) carry raw FFmpeg pointers and therefore need
//! an explicit `unsafe impl Send`. The justification is always at the C-API
//! level, never at the Rust-borrow level:
//!
//! - `AVBufferRef` refcounts are `atomic_uint` (`libavutil/buffer.c`), so
//! `av_buffer_ref` / `av_buffer_unref` are safe to call concurrently.
//! - `AVHWDeviceContext` (VAAPI `VADisplay`) is designed by FFmpeg/libva to
//! be shared across codec and filter contexts, including FFmpeg-internal
//! codec worker threads.
//! - `AVHWFramesContext` allocates from an `AVBufferPool` whose get/put are
//! atomic; `av_hwframe_get_buffer` is safe to call concurrently on
//! distinct frames.
//! - `SwsContext`, `AVFilterGraph`, `AVFrame`, `AVCodecContext` are NOT
//! thread-safe for concurrent use, but are `Send`-sound under the
//! single-thread exclusive access invariant that the encode worker
//! enforces.
//!
//! **Anti-pattern**: justifying `Send` with "`&mut self` ensures exclusive
//! access". `Send` is about *moving ownership between threads*, not about
//! borrowing. The `&mut self` on Rust methods is API convenience and is not
//! the basis for soundness — refs cloned via `ref_clone()` routinely escape
//! to other threads / FFmpeg-internal workers, and that is fine because the
//! underlying C APIs are designed for it.
use std::path::Path;
use anyhow::Result;
+4 -2
View File
@@ -13,8 +13,10 @@ pub struct SwEncState {
encode: SwEncEncode,
}
// SAFETY: SwEncState owns import and encode state exclusively and existing sync callers move it
// between threads only with external serialization; all FFI handles are accessed through &mut self.
// SAFETY: SwEncState is moved to a single encode thread and accessed only there.
// All FFmpeg handles (SwsContext, AVFrame, AVCodecContext) inside SwEncImport /
// SwEncEncode are non-thread-safe but Send-sound under exclusive access.
// Existing sync callers move it across threads only with external serialization.
unsafe impl Send for SwEncState {}
impl SwEncState {