diff --git a/crates/pf-client-core/src/clipboard.rs b/crates/pf-client-core/src/clipboard.rs index 55dd2eeb..cdaa50be 100644 --- a/crates/pf-client-core/src/clipboard.rs +++ b/crates/pf-client-core/src/clipboard.rs @@ -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 = 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 { // 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(()) } diff --git a/crates/pf-client-core/src/gamepad.rs b/crates/pf-client-core/src/gamepad.rs index 015fc0bd..1b26b84a 100644 --- a/crates/pf-client-core/src/gamepad.rs +++ b/crates/pf-client-core/src/gamepad.rs @@ -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); } diff --git a/crates/pf-client-core/src/lib.rs b/crates/pf-client-core/src/lib.rs index ec88584c..75cb2555 100644 --- a/crates/pf-client-core/src/lib.rs +++ b/crates/pf-client-core/src/lib.rs @@ -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)] diff --git a/crates/pf-client-core/src/video.rs b/crates/pf-client-core/src/video.rs index 0cfb4563..f10a91d2 100644 --- a/crates/pf-client-core/src/video.rs +++ b/crates/pf-client-core/src/video.rs @@ -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) }; } } diff --git a/crates/pf-client-core/src/video_d3d11.rs b/crates/pf-client-core/src/video_d3d11.rs index 1fc42978..6609c22f 100644 --- a/crates/pf-client-core/src/video_d3d11.rs +++ b/crates/pf-client-core/src/video_d3d11.rs @@ -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 = 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 = None; let mut fallback: Option = 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::() { // 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> { 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 { 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); diff --git a/crates/pf-client-core/src/video_software.rs b/crates/pf-client-core/src/video_software.rs index 57ff2908..e8646969 100644 --- a/crates/pf-client-core/src/video_software.rs +++ b/crates/pf-client-core/src/video_software.rs @@ -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( diff --git a/crates/pf-client-core/src/video_vaapi.rs b/crates/pf-client-core/src/video_vaapi.rs index 000c5ffc..df28c24a 100644 --- a/crates/pf-client-core/src/video_vaapi.rs +++ b/crates/pf-client-core/src/video_vaapi.rs @@ -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 { 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> { 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 { 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); diff --git a/crates/pf-client-core/src/video_vulkan.rs b/crates/pf-client-core/src/video_vulkan.rs index a0f96b80..f5ab3473 100644 --- a/crates/pf-client-core/src/video_vulkan.rs +++ b/crates/pf-client-core/src/video_vulkan.rs @@ -113,6 +113,10 @@ impl VulkanDecoder { vk: &VulkanDecodeDevice, ) -> Result { 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> { 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 { 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 { diff --git a/crates/pf-ffvk/src/lib.rs b/crates/pf-ffvk/src/lib.rs index bd4e1eff..6ced96e1 100644 --- a/crates/pf-ffvk/src/lib.rs +++ b/crates/pf-ffvk/src/lib.rs @@ -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);