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
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:
+16
-6
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user