fix(client-core,ffvk): close the proof-lint hole in two of the three unguarded crates

`clippy::undocumented_unsafe_blocks` is what makes the SAFETY convention a rule rather than a habit,
and three crates had never adopted it — pf-client-core (91 unsafe items), pf-presenter (123) and
punktfunk-core (167) — while every other subsystem crate denied it. That gap is why the decoders'
`unsafe impl Send`s carried a one-line aside instead of an argument: nothing required one.

pf-client-core and pf-ffvk now deny it, with a proof written for all 58 + 3 sites they had.

⚠️ 44 of those 58 were WINDOWS-ONLY — `clipboard.rs` 24 and `video_d3d11.rs` 20 — and invisible to
the Linux measurement that sized this work at 14. Same trap as the E0133 sweep: a Linux-only survey
of a cross-platform crate undercounts by whatever the `cfg` hides, here by 3x. Landing the deny on
the strength of that number alone would have re-broken Windows CI, which is exactly the mistake this
session already made once with the `warn`-that-was-really-`deny`.

The proofs say what is actually load-bearing rather than restating the call. In `clipboard.rs` that
is the ownership split Win32 requires and nothing in the code stated: `GetClipboardData` returns a
handle BORROWED from the clipboard (never freed here), while `GlobalAlloc` + `SetClipboardData`
TRANSFERS ownership to it (which is why nothing frees that one either) — two opposite rules, three
lines apart. In `video_d3d11.rs` the recurring one is that libav's `get_format` list is
NUL-terminated by `AV_PIX_FMT_NONE`, which is what keeps the walk in bounds.

Remaining: punktfunk-core (~146, of which `abi.rs` is 141) and pf-presenter (~108). Both want the
"state the contract once" treatment — abi.rs's sites are a handful of repeating shapes (`opt_cstr`
on caller C strings, null-guarded out-param writes, forwarding calls), not 141 distinct arguments.
Note the vendored `fec-rs` (18 sites) is a separate path-dependency crate, so it is out of scope
rather than something to prove.

Verified: Linux .21 fmt + both CI clippy steps rc=0; Windows .47 `-p pf-client-core` clippy
`-D warnings` rc=0 (the only place the 44 are visible), plus the full Windows CI clippy set and
pf-capture's 18 tests.
This commit is contained in:
2026-07-29 08:48:42 +02:00
parent 192635545d
commit e1ddd49e37
9 changed files with 123 additions and 0 deletions
+24
View File
@@ -292,6 +292,7 @@ mod os {
/// A registered clipboard format id, by name (`PNG`, the concealed marker, …).
fn registered(name: &str) -> u32 {
let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
// SAFETY: `wide` is a local NUL-terminated UTF-16 buffer that outlives this synchronous call.
unsafe { RegisterClipboardFormatW(PCWSTR(wide.as_ptr())) }
}
@@ -305,6 +306,7 @@ mod os {
fn open() -> Result<Clip> {
// A retry loop: another app can hold the clipboard for a moment.
for _ in 0..10 {
// SAFETY: takes no pointer; `None` is the documented "associate with no window" argument.
if unsafe { OpenClipboard(None) }.is_ok() {
return Ok(Clip);
}
@@ -315,26 +317,31 @@ mod os {
}
impl Drop for Clip {
fn drop(&mut self) {
// SAFETY: pairs with the `OpenClipboard` that built this guard; closing is what `Drop` is for.
let _ = unsafe { CloseClipboard() };
}
}
pub fn sequence_number() -> u32 {
// SAFETY: a no-argument query that only reads the OS clipboard's sequence counter.
unsafe { GetClipboardSequenceNumber() }
}
/// Password managers mark secrets with this format; §5.2's concealed-type rule.
pub fn is_concealed() -> bool {
let fmt = registered("ExcludeClipboardContentFromMonitorProcessing");
// SAFETY: a scalar format id in, a status out; reads nothing through a pointer.
unsafe { IsClipboardFormatAvailable(fmt) }.is_ok()
}
/// The wire kinds the current clipboard can supply, with size hints where they're free.
pub fn available_kinds() -> Vec<(&'static str, u64)> {
let mut out = Vec::new();
// SAFETY: as above — a scalar format id, status out.
if unsafe { IsClipboardFormatAvailable(CF_UNICODETEXT) }.is_ok() {
out.push((MIME_TEXT, 0));
}
// SAFETY: as above — a scalar format id, status out.
if unsafe { IsClipboardFormatAvailable(png_format()) }.is_ok() {
out.push((MIME_PNG, 0));
}
@@ -346,32 +353,43 @@ mod os {
let _clip = Clip::open()?;
match mime {
MIME_TEXT => {
// SAFETY: the clipboard is open (the `Clip` guard); the handle returned is BORROWED from the clipboard and stays valid while it is open, so it is never freed here.
let h = unsafe { GetClipboardData(CF_UNICODETEXT) }?;
let g = HGLOBAL(h.0);
// SAFETY: `g` is that borrowed clipboard handle; `GlobalLock` yields a pointer valid until the matching `GlobalUnlock` below.
let p = unsafe { GlobalLock(g) } as *const u16;
if p.is_null() {
bail!("clipboard text lock failed");
}
// GlobalSize is a byte count of a NUL-terminated UTF-16 buffer.
// SAFETY: a size query on the same live handle.
let bytes = unsafe { GlobalSize(g) };
let mut len = bytes / 2;
// SAFETY: `p` is the locked buffer and `len` is derived from the `GlobalSize` above, so the slice stays inside the allocation; it is read before the unlock.
let slice = unsafe { std::slice::from_raw_parts(p, len) };
if let Some(nul) = slice.iter().position(|&c| c == 0) {
len = nul;
}
// SAFETY: as above — same locked buffer and same length, read before the unlock.
let text = String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(p, len) });
// SAFETY: releases the lock taken above, exactly once.
let _ = unsafe { GlobalUnlock(g) };
Ok(text.into_bytes())
}
MIME_PNG => {
// SAFETY: as the text path — the handle is borrowed from the open clipboard, never freed here.
let h = unsafe { GetClipboardData(png_format()) }?;
let g = HGLOBAL(h.0);
// SAFETY: `g` is that borrowed handle; the pointer is valid until the matching unlock.
let p = unsafe { GlobalLock(g) } as *const u8;
if p.is_null() {
bail!("clipboard png lock failed");
}
// SAFETY: a size query on the same live handle.
let len = unsafe { GlobalSize(g) };
// SAFETY: `p` is the locked buffer and `len` came from `GlobalSize`, so the slice is in bounds; it is copied out before the unlock.
let out = unsafe { std::slice::from_raw_parts(p, len) }.to_vec();
// SAFETY: releases the lock taken above, exactly once.
let _ = unsafe { GlobalUnlock(g) };
Ok(out)
}
@@ -392,15 +410,21 @@ mod os {
other => bail!("unsupported clipboard format {other}"),
};
let _clip = Clip::open()?;
// SAFETY: no arguments; the clipboard is open and owned by this thread via the `Clip` guard.
unsafe { EmptyClipboard() }?;
// The clipboard OWNS this block once SetClipboardData succeeds — do not free it.
// SAFETY: a size in, an owned moveable handle out — ownership passes to the clipboard at `SetClipboardData` below.
let g = unsafe { GlobalAlloc(GMEM_MOVEABLE, payload.len()) }?;
// SAFETY: `g` is the handle just allocated; the pointer is valid until the matching unlock.
let p = unsafe { GlobalLock(g) } as *mut u8;
if p.is_null() {
bail!("clipboard alloc lock failed");
}
// SAFETY: `p` addresses that locked block, allocated at exactly `payload.len()` bytes on the line above, and the two regions are distinct allocations.
unsafe { std::ptr::copy_nonoverlapping(payload.as_ptr(), p, payload.len()) };
// SAFETY: releases the lock taken above, before the handle is handed to the clipboard.
let _ = unsafe { GlobalUnlock(g) };
// SAFETY: ownership of `g` transfers to the clipboard here, which is why nothing frees it afterwards.
unsafe { SetClipboardData(fmt, Some(HANDLE(g.0))) }?;
Ok(())
}
+2
View File
@@ -999,6 +999,8 @@ impl Worker {
fn set_slot_sensors(slot: &mut Slot, enabled: bool) {
use sdl3::sensor::SensorType;
for s in [SensorType::Gyroscope, SensorType::Accelerometer] {
// SAFETY: an SDL3 query on the gamepad this slot owns and keeps open; it takes a
// plain sensor-type enum and only reads device state.
if unsafe { slot.pad.has_sensor(s) } {
let _ = slot.pad.sensor_set_enabled(s, enabled);
}
+6
View File
@@ -14,6 +14,12 @@
//! so `crate::audio` is the only name the session pump ever sees. `keymap` (evdev-keyed)
//! stays Linux: the session path uses pf-presenter's SDL-scancode table instead.
// Unsafe-proof program: every `unsafe {}` / `unsafe impl` in this crate carries a `// SAFETY:`
// proof of why it is sound. This crate held ~91 unsafe items with NO enforcement while every
// other subsystem crate denied it — the decoders' `unsafe impl Send`s had a one-line aside
// instead of an argument precisely because nothing required one.
#![deny(clippy::undocumented_unsafe_blocks)]
#[cfg(target_os = "linux")]
pub mod audio;
#[cfg(windows)]
+2
View File
@@ -231,6 +231,8 @@ unsafe impl Send for DrmFrameGuard {}
impl Drop for DrmFrameGuard {
fn drop(&mut self) {
// SAFETY: `self.0` is the one `AVFrame` this guard owns; `av_frame_free` releases it
// exactly once (this `Drop` runs once) and nulls the pointer through the `&mut`.
unsafe { ffmpeg::ffi::av_frame_free(&mut self.0) };
}
}
+43
View File
@@ -165,6 +165,9 @@ unsafe extern "C" fn get_format_d3d11(
mut list: *const ffmpeg::ffi::AVPixelFormat,
) -> ffmpeg::ffi::AVPixelFormat {
use ffmpeg::ffi::*;
// SAFETY: libav calls this `get_format` callback with a context and a list it owns; the list
// is terminated by `AV_PIX_FMT_NONE`, so the walk stays inside it, and `avctx`'s fields are
// read/set only while libav holds it live for the call.
unsafe {
if (*avctx).hw_device_ctx.is_null() {
return AVPixelFormat::AV_PIX_FMT_NONE;
@@ -187,6 +190,8 @@ fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id)
let video: ID3D11VideoDevice = device
.cast()
.context("device lacks ID3D11VideoDevice (created without VIDEO_SUPPORT)")?;
// SAFETY: COM calls on the live `ID3D11VideoDevice` obtained by the checked `cast` above; the
// count bounds the loop and each profile is returned by value.
let profiles: Vec<GUID> = unsafe {
let n = video.GetVideoDecoderProfileCount();
(0..n)
@@ -200,6 +205,8 @@ fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id)
other => bail!("no DXVA profile known for {other:?}"),
};
let ok = profiles.contains(&wanted)
// SAFETY: same live video device; the two arguments are a borrowed local GUID and a plain
// format enum.
&& unsafe { video.CheckVideoDecoderFormat(&wanted, format) }
.map(|b| b.as_bool())
.unwrap_or(false);
@@ -210,6 +217,7 @@ fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id)
// decode error → software demotion + keyframe re-request path covers the switch.
if codec_id == ffmpeg::codec::Id::HEVC {
let main10 = profiles.contains(&PROFILE_HEVC_VLD_MAIN10)
// SAFETY: as above — borrowed static GUID plus a plain format enum.
&& unsafe { video.CheckVideoDecoderFormat(&PROFILE_HEVC_VLD_MAIN10, DXGI_FORMAT_P010) }
.map(|b| b.as_bool())
.unwrap_or(false);
@@ -225,6 +233,9 @@ fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id)
/// gap-free stream) instead of dying mid-stream on the opening IDR.
unsafe fn d3d11va_decode_supported(hw_device: *mut ffmpeg::ffi::AVBufferRef) -> bool {
use ffmpeg::ffi::*;
// SAFETY: `hw_device` is a valid `AVBufferRef` by this fn's contract; the frames context is
// allocated, configured and released within this scope, and every libav return is checked
// before use.
unsafe {
// Scope-bound: this probe owns the frames ctx for the length of the check and the drop
// below releases it on BOTH exits, instead of the early return relying on the null case and
@@ -250,13 +261,19 @@ unsafe fn d3d11va_decode_supported(hw_device: *mut ffmpeg::ffi::AVBufferRef) ->
/// keeps the shared textures on one GPU. `None`/no match falls back to the first hardware
/// adapter (single-GPU boxes; a WARP-only box fails out to software decode).
fn create_device(luid: Option<[u8; 8]>) -> Result<(ID3D11Device, ID3D11DeviceContext)> {
// SAFETY: DXGI factory creation takes no pointer and returns an owned factory or an error,
// checked by `?`.
let factory: IDXGIFactory1 = unsafe { CreateDXGIFactory1() }.context("CreateDXGIFactory1")?;
let mut chosen: Option<IDXGIAdapter1> = None;
let mut fallback: Option<IDXGIAdapter1> = None;
for i in 0.. {
// SAFETY: a COM call on the live factory; the `Ok` binding is what proves an adapter came
// back.
let Ok(adapter) = (unsafe { factory.EnumAdapters1(i) }) else {
break;
};
// SAFETY: a COM call on the adapter just enumerated, filling a descriptor returned by
// value.
let Ok(desc) = (unsafe { adapter.GetDesc1() }) else {
continue;
};
@@ -286,6 +303,8 @@ fn create_device(luid: Option<[u8; 8]>) -> Result<(ID3D11Device, ID3D11DeviceCon
.ok_or_else(|| anyhow!("no hardware DXGI adapter"))?;
let mut device = None;
let mut context = None;
// SAFETY: `adapter` is the live adapter chosen above; the two out-params are local `Option`s
// the callee only writes, and both are checked before use.
unsafe {
D3D11CreateDevice(
&adapter,
@@ -308,6 +327,8 @@ fn create_device(luid: Option<[u8; 8]>) -> Result<(ID3D11Device, ID3D11DeviceCon
// keeps the invariant obvious).
if let Ok(mt) = device.cast::<ID3D11Multithread>() {
// Returns the PREVIOUS protection state — nothing to act on.
// SAFETY: a COM call on the live `ID3D11Multithread` from a checked `cast`; it takes a
// BOOL and returns the previous state.
let _ = unsafe { mt.SetMultithreadProtected(true) };
}
Ok((device, context))
@@ -326,6 +347,8 @@ struct Slot {
impl Drop for Slot {
fn drop(&mut self) {
// SAFETY: `self.handle` is the shared-texture NT handle this slot owns; `Drop` runs once,
// so it is closed exactly once and never used after.
unsafe {
let _ = windows::Win32::Foundation::CloseHandle(self.handle);
}
@@ -374,8 +397,11 @@ impl SharedRing {
OutputHeight: height,
Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
};
// SAFETY: COM calls on the live video device, with a borrowed local descriptor and a
// checked out-param.
let enumerator = unsafe { video_device.CreateVideoProcessorEnumerator(&content) }
.context("CreateVideoProcessorEnumerator")?;
// SAFETY: same live device, borrowed enumerator from the line above.
let vp = unsafe { video_device.CreateVideoProcessor(&enumerator, 0) }
.context("CreateVideoProcessor")?;
@@ -407,6 +433,8 @@ impl SharedRing {
let mut slots = Vec::with_capacity(RING_SLOTS);
for _ in 0..RING_SLOTS {
let mut tex = None;
// SAFETY: a `?`-checked `CreateTexture2D` on the live device, over a fully-initialized
// stack descriptor and a live `Option` out-param.
unsafe { device.CreateTexture2D(&desc, None, Some(&mut tex)) }
.context("create shared hand-off texture")?;
let tex: ID3D11Texture2D = tex.expect("CreateTexture2D succeeded");
@@ -414,6 +442,8 @@ impl SharedRing {
tex.cast().context("shared texture lacks IDXGIKeyedMutex")?;
let resource: IDXGIResource1 =
tex.cast().context("shared texture lacks IDXGIResource1")?;
// SAFETY: the shared-handle creation runs on the live texture just created; the
// returned NT handle is owned by the `Slot` built below, which closes it in `Drop`.
let handle = unsafe {
resource.CreateSharedHandle(
None,
@@ -428,6 +458,8 @@ impl SharedRing {
..Default::default()
};
let mut out_view = None;
// SAFETY: COM calls on the live video device with borrowed local descriptors and a
// checked out-param.
unsafe {
video_device.CreateVideoProcessorOutputView(
&tex,
@@ -518,6 +550,9 @@ impl D3d11vaDecoder {
let video_context1: ID3D11VideoContext1 = context
.cast()
.context("context lacks ID3D11VideoContext1 (pre-1703 Windows?)")?;
// SAFETY: a self-contained builder: every libav allocation is made here and null-checked,
// the D3D11VA hwctx fields are filled from the live device/context borrowed above, and
// what survives is moved into the decoder, which frees each exactly once in `Drop`.
unsafe {
let hw_device =
ffi::av_hwdevice_ctx_alloc(ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA);
@@ -579,6 +614,9 @@ impl D3d11vaDecoder {
pub(crate) fn decode(&mut self, au: &[u8]) -> Result<Option<D3d11Frame>> {
use ffmpeg::ffi;
// SAFETY: `packet`/`frame`/`ctx` are this decoder's own allocations, live for its whole
// lifetime; `au` outlives the synchronous copy out of it, and every libav return is
// checked before use.
unsafe {
let r = ffi::av_new_packet(self.packet, au.len() as i32);
if r < 0 {
@@ -616,6 +654,8 @@ impl D3d11vaDecoder {
/// stream runs `RING_SLOTS` ahead of present).
fn lift(&mut self) -> Result<D3d11Frame> {
use ffmpeg::ffi;
// SAFETY: `self.frame` is this decoder's own `AVFrame`; the format check below is what
// proves it carries a D3D11 texture before anything reads the surface out of it.
unsafe {
if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_D3D11 as i32 {
bail!("decoder returned a software frame (no D3D11 surface)");
@@ -794,6 +834,9 @@ impl D3d11vaDecoder {
impl Drop for D3d11vaDecoder {
fn drop(&mut self) {
use ffmpeg::ffi;
// SAFETY: each pointer is this decoder's own allocation and nothing else holds it; `Drop`
// runs exactly once and each free nulls its pointer through the `&mut`, so none can be
// released twice.
unsafe {
ffi::av_packet_free(&mut self.packet);
ffi::av_frame_free(&mut self.frame);
@@ -23,6 +23,9 @@ impl SoftwareDecoder {
let codec = ffmpeg::decoder::find(codec_id)
.ok_or_else(|| anyhow!("no {codec_id:?} decoder in libavcodec"))?;
let mut ctx = ffmpeg::codec::Context::new_with_codec(codec);
// SAFETY: `as_mut_ptr` yields the `AVCodecContext` behind the `ctx` allocated on the line
// above, which outlives these writes; each store is an in-bounds scalar field write on that
// live context, made before the decoder is opened and reads them.
unsafe {
let raw = ctx.as_mut_ptr();
(*raw).flags |= ffmpeg::ffi::AV_CODEC_FLAG_LOW_DELAY as i32;
@@ -70,6 +73,9 @@ impl SoftwareDecoder {
5 | 6 => SWS_CS_ITU601,
_ => SWS_CS_ITU709,
};
// SAFETY: `sws_getCoefficients` returns a pointer into libav's own static coefficient
// tables — valid for the process, read-only — and `sws_setColorspaceDetails` takes it
// plus the live `SwsContext` behind `ctx` and plain scalars.
unsafe {
let coeffs = ffmpeg::ffi::sws_getCoefficients(cs);
ffmpeg::ffi::sws_setColorspaceDetails(
+14
View File
@@ -19,6 +19,9 @@ unsafe extern "C" fn pick_vaapi(
_ctx: *mut ffmpeg::ffi::AVCodecContext,
mut list: *const ffmpeg::ffi::AVPixelFormat,
) -> ffmpeg::ffi::AVPixelFormat {
// SAFETY: libav calls this `get_format` callback with a list it owns, terminated by
// `AV_PIX_FMT_NONE` — the walk stops at that terminator, so it stays inside the array, and it
// only reads.
unsafe {
while *list != ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_NONE {
if *list == ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_VAAPI {
@@ -59,6 +62,9 @@ unsafe impl Send for VaapiDecoder {}
impl VaapiDecoder {
pub(crate) fn new(codec_id: ffmpeg::codec::Id) -> Result<VaapiDecoder> {
use ffmpeg::ffi;
// SAFETY: a self-contained builder — every allocation below is made here, each result is
// checked before the next call uses it, and the ones that survive are moved into the
// returned `VaapiDecoder`, which frees each exactly once in `Drop`.
unsafe {
let mut hw_device: *mut ffi::AVBufferRef = ptr::null_mut();
let r = ffi::av_hwdevice_ctx_create(
@@ -109,6 +115,9 @@ impl VaapiDecoder {
pub(crate) fn decode(&mut self, au: &[u8]) -> Result<Option<DmabufFrame>> {
use ffmpeg::ffi;
// SAFETY: `packet`/`frame`/`ctx` are this decoder's own allocations, live for its whole
// lifetime; `au` outlives the synchronous `send_packet` that copies out of it, and every
// libav return is checked before the result is used.
unsafe {
let r = ffi::av_new_packet(self.packet, au.len() as i32);
if r < 0 {
@@ -147,6 +156,8 @@ impl VaapiDecoder {
/// `DRM_FORMAT_NV12`) and flatten every plane across every layer in order (Y then UV).
fn map_dmabuf(&mut self) -> Result<DmabufFrame> {
use ffmpeg::ffi;
// SAFETY: `self.frame` is this decoder's own `AVFrame`, holding a decoded VAAPI surface —
// the format check below is what proves that before anything reads the hardware layout.
unsafe {
if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_VAAPI as i32 {
bail!("decoder returned a software frame (no VAAPI surface)");
@@ -247,6 +258,9 @@ fn log_descriptor_once(
impl Drop for VaapiDecoder {
fn drop(&mut self) {
use ffmpeg::ffi;
// SAFETY: each pointer is this decoder's own allocation and nothing else holds it; `Drop`
// runs exactly once, and each free nulls the pointer through its `&mut`, so none can be
// released twice. Freed packet-then-frame-then-context, the order libav documents.
unsafe {
ffi::av_packet_free(&mut self.packet);
ffi::av_frame_free(&mut self.frame);
+16
View File
@@ -113,6 +113,10 @@ impl VulkanDecoder {
vk: &VulkanDecodeDevice,
) -> Result<VulkanDecoder> {
use ffmpeg::ffi;
// SAFETY: a self-contained builder — every allocation is made here and null-checked before
// use, the `AVVulkanDeviceContext` fields are filled from `vk`'s live handles and from
// `_ctx_storage`, which the decoder keeps alive alongside the context, and what survives is
// moved into the returned `VulkanDecoder`, which frees each exactly once in `Drop`.
unsafe {
let mut hw_device =
ffi::av_hwdevice_ctx_alloc(ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VULKAN);
@@ -273,6 +277,9 @@ impl VulkanDecoder {
pub(crate) fn decode(&mut self, au: &[u8]) -> Result<Option<VkVideoFrame>> {
use ffmpeg::ffi;
// SAFETY: `packet`/`frame`/`ctx` are this decoder's own allocations, live for its whole
// lifetime; `au` outlives the synchronous `send_packet` that copies out of it, and every
// libav return is checked before the result is used.
unsafe {
let r = ffi::av_new_packet(self.packet, au.len() as i32);
if r < 0 {
@@ -327,6 +334,9 @@ impl VulkanDecoder {
/// at its own submit time.
fn extract(&mut self) -> Result<VkVideoFrame> {
use ffmpeg::ffi;
// SAFETY: `self.frame` is this decoder's own `AVFrame`; the format check below is what
// proves it carries an `AVVkFrame` before anything reads the Vulkan image out of it, and
// the clone handed onward keeps the image + frames context alive through present.
unsafe {
if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_VULKAN as i32 {
bail!("decoder returned a non-Vulkan frame");
@@ -387,6 +397,9 @@ impl VulkanDecoder {
impl Drop for VulkanDecoder {
fn drop(&mut self) {
use ffmpeg::ffi;
// SAFETY: each pointer is this decoder's own allocation and nothing else holds it; `Drop`
// runs exactly once, and each free nulls the pointer through its `&mut`, so none can be
// released twice. Freed packet-then-frame-then-context, the order libav documents.
unsafe {
ffi::av_packet_free(&mut self.packet);
ffi::av_frame_free(&mut self.frame);
@@ -406,6 +419,9 @@ unsafe extern "C" fn pick_vulkan(
mut list: *const ffmpeg::ffi::AVPixelFormat,
) -> ffmpeg::ffi::AVPixelFormat {
use ffmpeg::ffi;
// SAFETY: libav calls this `get_format` callback with a list it owns, terminated by
// `AV_PIX_FMT_NONE` — the walk stops at that terminator, so it stays inside the array, and it
// only reads.
unsafe {
let mut offered = false;
while *list != ffi::AVPixelFormat::AV_PIX_FMT_NONE {
+10
View File
@@ -6,6 +6,8 @@
//! conversion helpers below cross between them and ash's u64-newtype handles; both sides
//! are the same underlying Vulkan object handles, so the casts are value-preserving.
// Unsafe-proof program: every `unsafe {}` here carries a `// SAFETY:` proof.
#![deny(clippy::undocumented_unsafe_blocks)]
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
@@ -63,6 +65,9 @@ pub mod ashx {
*const std::ffi::c_char,
) -> ash::vk::PFN_vkVoidFunction,
) -> PFN_vkGetInstanceProcAddr {
// SAFETY: both sides are `extern "system"` fn pointers with the identical signature —
// `(VkInstance, *const c_char) -> PFN_vkVoidFunction`. The transmute only reinterprets the
// ash-side type alias as our bindgen-side one, which are the same ABI type.
unsafe { std::mem::transmute(f) }
}
}
@@ -75,6 +80,9 @@ mod tests {
/// readable at the offsets bindgen computed — sem_value zero-initialized.
#[test]
fn vk_frame_alloc_links_and_zeroes() {
// SAFETY: `av_vk_frame_alloc` is libavutil's own allocator and returns either null —
// asserted against before any field is read — or a zero-initialized `AVVkFrame` valid for
// the reads below. The frame is deliberately leaked, so nothing frees it twice.
unsafe {
let f = av_vk_frame_alloc();
assert!(!f.is_null(), "av_vk_frame_alloc returned NULL");
@@ -87,6 +95,8 @@ mod tests {
/// AV_NUM_DATA_POINTERS-sized arrays came through with the right length.
#[test]
fn frame_arrays_are_av_num_data_pointers() {
// SAFETY: `AVVkFrame` is a `repr(C)` POD of scalars, handles and fixed-size arrays, so
// all-zeroes is a valid bit pattern for it; the test only reads array lengths.
let f: AVVkFrame = unsafe { std::mem::zeroed() };
assert_eq!(f.img.len(), 8);
assert_eq!(f.sem_value.len(), 8);