diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 838de026..6a824cc6 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -111,6 +111,13 @@ jobs: - name: Format run: cargo fmt --all --check + # rust-safety WP2c: three textual gates for classes no lint covers — unsafe fn markers + # carrying no contract, panic across an extern boundary (an abort since 1.81), and + # process-global safe APIs (env::set_var & co, count-ratcheted). Pure grep/awk, no cargo. + # Both failure modes were demonstrated before this became blocking (planted instances). + - name: Unsafe-hygiene grep gates + run: sh scripts/ci/check-unsafe-hygiene.sh + - name: Clippy (deny warnings) run: cargo clippy --workspace --all-targets --locked -- -D warnings @@ -139,8 +146,8 @@ jobs: # `nvenc` gates enc/linux/nvenc_cuda.rs (+ nvenc_core/nvenc_status) and `vulkan-encode` gates # enc/linux/vulkan_video.rs (+ the vendored vk_av1_encode/vk_valve_rgb bindings) — ~8,150 # lines carrying ~70 `unsafe` blocks. Their ONLY prior CI coverage was deb.yml's - # `cargo build`, where warnings are not errors, so pf-encode's own - # `#![deny(clippy::undocumented_unsafe_blocks)]` — the crate's stated unsafe-proof gate — + # `cargo build`, where warnings are not errors, so the `undocumented_unsafe_blocks` deny + # (now hoisted into [workspace.lints]) — pf-encode's stated unsafe-proof gate — # was never actually enforced on them. (`pyrowave` needs no extra step: punktfunk-host has # `default = ["pyrowave"]`, so the steps above already cover it.) # diff --git a/.gitea/workflows/windows-drivers.yml b/.gitea/workflows/windows-drivers.yml index ca74f3c2..e77da010 100644 --- a/.gitea/workflows/windows-drivers.yml +++ b/.gitea/workflows/windows-drivers.yml @@ -159,9 +159,10 @@ jobs: # The gamepad drivers' business logic is 100% safe (it moved onto pf-umdf-util, the audited # unsafe layer); pf-vdisplay + wdk-iddcx are inherently FFI-bound but every `unsafe {}` carries a # `// SAFETY:` proof. Both invariants are lint-gated (`unsafe_op_in_unsafe_fn` + - # `undocumented_unsafe_blocks`); this step keeps them from regressing. (wdk-probe is a - # toolchain-only probe crate and is excluded.) - run: cargo clippy -p pf-umdf-util -p pf-xusb -p pf-gamepad -p pf-mouse -p wdk-iddcx -p pf-vdisplay --all-targets -- -D warnings + # `undocumented_unsafe_blocks`); this step keeps them from regressing. wdk-probe is a + # toolchain-only probe crate, but it holds real DDI slot-dispatch unsafe (iddcx_rt.rs), so it + # runs the same gates. + run: cargo clippy -p pf-umdf-util -p pf-xusb -p pf-gamepad -p pf-mouse -p wdk-iddcx -p pf-vdisplay -p wdk-probe --all-targets -- -D warnings - name: cargo fmt --check the safe-layer + gamepad/mouse drivers run: cargo fmt -p pf-umdf-util -p pf-xusb -p pf-gamepad -p pf-mouse --check - name: Inspect /INTEGRITYCHECK (before) — expect FORCE_INTEGRITY set by wdk-build diff --git a/Cargo.toml b/Cargo.toml index c4cc9afc..97e4ef20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,6 +101,17 @@ repository = "https://git.unom.io/unom/punktfunk" [workspace.lints.rust] unsafe_op_in_unsafe_fn = "deny" +# The companion lint: every `unsafe {}` / `unsafe impl` carries a `// SAFETY:` proof. Hoisted here +# from ~85 per-file `#![deny(...)]` attributes so a NEW crate (or a new module in an old one) is +# covered on creation rather than on remembering — the per-file form left pf-vkhdr-layer, +# wdk-probe, and half of pf-clipboard uncovered for months. NOTE: this table reaches only crates +# with `[lints] workspace = true`; `packaging/windows/drivers` and `packaging/windows/pf-vkhdr-layer` +# are SEPARATE workspaces and restate it (any "workspace-wide" claim must be made three times or it +# is false). Of the members, only the two vendored snapshots (pf-bitstream/vendor/cros-codecs, +# punktfunk-host/vendor/usbip-sim) stay out, deliberately — upstream code stays pristine. +[workspace.lints.clippy] +undocumented_unsafe_blocks = "deny" + [profile.release] opt-level = 3 lto = "thin" diff --git a/clients/android/native/src/session/connect.rs b/clients/android/native/src/session/connect.rs index 98480361..42ec297d 100644 --- a/clients/android/native/src/session/connect.rs +++ b/clients/android/native/src/session/connect.rs @@ -9,7 +9,7 @@ use punktfunk_core::config::{CompositorPref, GamepadPref, Mode}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use super::{hex32, jni_guard, parse_hex32, SessionHandle}; +use super::{hex32, jni_guard, lock_recover, parse_hex32, SessionHandle}; /// Machine token of the most recent `nativeConnect`/`nativePair` failure, taken (and cleared) /// by `nativeTakeLastError` so Kotlin can render a cause-specific message instead of the old @@ -41,7 +41,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeTakeLastErr env: JNIEnv<'local>, _this: JObject<'local>, ) -> jni::sys::jstring { - let token = std::mem::take(&mut *LAST_ERROR.lock().unwrap()); + let token = std::mem::take(&mut *lock_recover(&LAST_ERROR)); match env.new_string(token) { Ok(s) => s.into_raw(), Err(_) => JObject::null().into_raw(), diff --git a/clients/android/native/src/session/mod.rs b/clients/android/native/src/session/mod.rs index 9875d4a5..f38dac1e 100644 --- a/clients/android/native/src/session/mod.rs +++ b/clients/android/native/src/session/mod.rs @@ -45,6 +45,15 @@ pub(crate) fn jni_guard(default: T, f: impl FnOnce() -> T) -> T { }) } +/// Poison-recovering lock for the JNI entry points that are NOT behind [`jni_guard`]: a +/// `.lock().unwrap()` there turns a poisoned mutex into a panic across the `extern "system"` +/// boundary — an abort of the whole app on Rust ≥ 1.81 (the panic-in-extern grep gate's class). +/// The slots behind these mutexes are plane-thread handles and last-value caches; whatever a +/// poisoned writer left is still valid to inspect or replace. +pub(crate) fn lock_recover(m: &Mutex) -> std::sync::MutexGuard<'_, T> { + m.lock().unwrap_or_else(std::sync::PoisonError::into_inner) +} + /// A live session behind the `jlong` handle: the connector + the decode thread it feeds. pub(crate) struct SessionHandle { // Read only by the android decode path (`nativeStartVideo` → `crate::decode`); on the host diff --git a/clients/android/native/src/session/planes.rs b/clients/android/native/src/session/planes.rs index 198ea461..5a62288a 100644 --- a/clients/android/native/src/session/planes.rs +++ b/clients/android/native/src/session/planes.rs @@ -8,7 +8,7 @@ use jni::objects::JString; use jni::sys::{jboolean, jdoubleArray, jintArray, jlong, jsize, jstring}; use jni::JNIEnv; -use super::{jni_guard, SessionHandle}; +use super::{jni_guard, lock_recover, SessionHandle}; /// `NativeBridge.nativeStartVideo(handle, surface, decoderName, lowLatencyMode, lowLatencyFeature, /// isTv, presentPriority, smoothBuffer)` — wrap the SurfaceView's `Surface` as an `ANativeWindow` @@ -48,7 +48,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartVideo( .filter(|s| !s.is_empty()); // SAFETY: live handle per the nativeConnect/nativeClose contract. let h = unsafe { &*(handle as *const SessionHandle) }; - let mut guard = h.video.lock().unwrap(); + let mut guard = lock_recover(&h.video); if guard.is_some() { return; // already streaming } @@ -222,7 +222,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoStats( } // SAFETY: live handle per the nativeConnect/nativeClose contract. let h = unsafe { &*(handle as *const SessionHandle) }; - if h.video.lock().unwrap().is_none() { + if lock_recover(&h.video).is_none() { return std::ptr::null_mut(); // not streaming → no stats } let snap = h @@ -385,7 +385,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartAudio( } // SAFETY: live handle per the nativeConnect/nativeClose contract. let h = unsafe { &*(handle as *const SessionHandle) }; - let mut guard = h.audio.lock().unwrap(); + let mut guard = lock_recover(&h.audio); if guard.is_some() { return; // already playing } @@ -434,7 +434,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartMic( } // SAFETY: live handle per the nativeConnect/nativeClose contract. let h = unsafe { &*(handle as *const SessionHandle) }; - let mut guard = h.mic.lock().unwrap(); + let mut guard = lock_recover(&h.mic); if let Some(m) = guard.as_ref() { return m.session_id(); // already capturing — same stream, same session } @@ -516,7 +516,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartPadAud speaker != 0, ) { Some(p) => { - *h.pad_audio.lock().unwrap() = Some(p); + *lock_recover(&h.pad_audio) = Some(p); 1 } None => 0, @@ -629,6 +629,6 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeMicActive( } // SAFETY: live handle per the nativeConnect/nativeClose contract. let h = unsafe { &*(handle as *const SessionHandle) }; - jboolean::from(h.mic.lock().unwrap().is_some()) + jboolean::from(lock_recover(&h.mic).is_some()) }) } diff --git a/clients/windows/src/deeplink.rs b/clients/windows/src/deeplink.rs index 87ea4a40..eb06bdde 100644 --- a/clients/windows/src/deeplink.rs +++ b/clients/windows/src/deeplink.rs @@ -176,7 +176,13 @@ unsafe extern "system" fn wnd_proc( let slice = unsafe { std::slice::from_raw_parts(cds.lpData as *const u16, len) }; let url = String::from_utf16_lossy(slice); tracing::debug!(%url, "link from another instance"); - INBOX.lock().unwrap().push(url); + // Poison-recover, never unwrap: a panic out of a window procedure is an abort since + // Rust 1.81, and the inbox is a plain Vec that stays valid whatever a poisoned + // writer left behind. + INBOX + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push(url); return LRESULT(1); } } diff --git a/clients/windows/src/main.rs b/clients/windows/src/main.rs index 110719b0..278c3a94 100644 --- a/clients/windows/src/main.rs +++ b/clients/windows/src/main.rs @@ -15,7 +15,6 @@ //! (measure the path: probe burst → goodput / loss / recommended bitrate) // Unsafe-proof program: every `unsafe {}` in this client carries a `// SAFETY:` proof. -#![deny(clippy::undocumented_unsafe_blocks)] // Link as a GUI (windows) subsystem binary so the default windowed launch (MSIX / double-click) // does NOT pop a console window. The CLI paths (--headless/--discover) reattach to the launching // terminal's console at startup (see main), so their output is still visible when run from a shell. diff --git a/crates/libvpl-sys/src/lib.rs b/crates/libvpl-sys/src/lib.rs index 56972208..db12ddca 100644 --- a/crates/libvpl-sys/src/lib.rs +++ b/crates/libvpl-sys/src/lib.rs @@ -10,6 +10,11 @@ #![allow(non_snake_case)] // Bindgen output for a C API: u128 layout warnings and the like are upstream's concern. #![allow(improper_ctypes)] +// The workspace-wide undocumented_unsafe_blocks deny cannot apply to GENERATED code: bindgen +// emits `unsafe {}` in layout tests/accessors and nobody hand-writes proofs into OUT_DIR. This +// crate is bindings-only by charter (the safe wrapper lives with the consumer), so the allow is +// crate-wide; the hand-written link-sanity test below still carries its proof by convention. +#![allow(clippy::undocumented_unsafe_blocks)] // Generated code — clippy findings in it (missing safety docs on generated unsafe fns, style // nits across 14k lines) are bindgen's shape, not ours; the safe wrapper in pf-encode is the // linted surface. @@ -27,6 +32,8 @@ mod tests { /// implementations — that's fine, MFXLoad itself must still succeed). #[test] fn dispatcher_links_and_loads() { + // SAFETY: MFXLoad allocates the dispatcher's loader context (documented to work with no + // driver present) and MFXUnload frees that same non-null handle; nothing else is touched. unsafe { let loader = MFXLoad(); assert!(!loader.is_null(), "MFXLoad returned NULL"); diff --git a/crates/pf-capture/src/lib.rs b/crates/pf-capture/src/lib.rs index becd589e..153026fb 100644 --- a/crates/pf-capture/src/lib.rs +++ b/crates/pf-capture/src/lib.rs @@ -7,13 +7,6 @@ //! [`FrameChannelSender`] closure, so this crate reaches neither the encoder nor the host //! orchestrator). -// Every unsafe block in this crate carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] -// …and that program only covers a whole `unsafe fn` body once the body needs its own block: in -// edition 2021 `unsafe_op_in_unsafe_fn` is allow-by-default, which exempted the crate's hardest FFI -// (the ring/slot construction, the channel broker, every D3D converter ctor) from the deny above. -#![deny(unsafe_op_in_unsafe_fn)] - use anyhow::Result; use pf_frame::{CapturedFrame, FramePayload, PixelFormat}; // The Linux capturer reaches `DmabufFrame` through `super::`; `CursorOverlay` it names directly as diff --git a/crates/pf-capture/src/linux/mod.rs b/crates/pf-capture/src/linux/mod.rs index 6a28ff21..6099aa29 100644 --- a/crates/pf-capture/src/linux/mod.rs +++ b/crates/pf-capture/src/linux/mod.rs @@ -25,7 +25,6 @@ // Every `unsafe` block in this module TREE carries a `// SAFETY:` proof; enforce it (unsafe-proof // program). This file itself has none — the FFI lives in the child modules declared at the bottom // (`pipewire`, `pw_cursor`, `pw_pods`, `portal`, `xfixes_cursor`), which this inner attribute covers. -#![deny(clippy::undocumented_unsafe_blocks)] use super::{CapturedFrame, Capturer, DmabufFrame, FramePayload, PixelFormat, ZeroCopyPolicy}; use anyhow::{anyhow, Context, Result}; diff --git a/crates/pf-capture/src/windows/dxgi.rs b/crates/pf-capture/src/windows/dxgi.rs index 190664a4..962b22b9 100644 --- a/crates/pf-capture/src/windows/dxgi.rs +++ b/crates/pf-capture/src/windows/dxgi.rs @@ -9,9 +9,6 @@ //! `crate::dxgi::*` path keeps resolving. DXGI Desktop Duplication has been removed; this //! module contains no capturer. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - pub use pf_frame::dxgi::{make_device, pack_luid, D3d11Frame, PyroFrameShare, WinCaptureTarget}; // The P010 colour self-test (sweep Phase 5.5) — the `hdr-p010-selftest` subcommand, its f64 diff --git a/crates/pf-capture/src/windows/idd_push.rs b/crates/pf-capture/src/windows/idd_push.rs index abeeae0e..1d1c3a3b 100644 --- a/crates/pf-capture/src/windows/idd_push.rs +++ b/crates/pf-capture/src/windows/idd_push.rs @@ -16,9 +16,6 @@ //! [`pf_driver_proto`] (which OWNS the contract, with `const` size asserts) — both sides `use` it, so //! drift is a compile error rather than a "must match" comment. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::dxgi::{ make_device, BgraToYuvPlanes, D3d11Frame, HdrP010Converter, HdrRgb10Converter, PyroFrameShare, VideoConverter, WinCaptureTarget, diff --git a/crates/pf-capture/src/windows/idd_push/channel.rs b/crates/pf-capture/src/windows/idd_push/channel.rs index f72e3476..3c99f3ce 100644 --- a/crates/pf-capture/src/windows/idd_push/channel.rs +++ b/crates/pf-capture/src/windows/idd_push/channel.rs @@ -2,9 +2,6 @@ //! capturer): duplicates the unnamed shared header / ring / event handles into the driver's WUDFHost //! and delivers them as bare handle values over the SYSTEM-only control device. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::*; /// The sealed channel's handle-duplication broker (`design/idd-push-security.md`): the frame objects diff --git a/crates/pf-capture/src/windows/idd_push/cursor.rs b/crates/pf-capture/src/windows/idd_push/cursor.rs index 80e79ae7..f630ae6a 100644 --- a/crates/pf-capture/src/windows/idd_push/cursor.rs +++ b/crates/pf-capture/src/windows/idd_push/cursor.rs @@ -5,9 +5,6 @@ //! [`pf_frame::CursorOverlay`] the Linux portal path produces — everything downstream (the //! cursor forwarder, the wire, the client renderer) is shared. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] - use super::*; use pf_driver_proto::cursor::{ CursorShm, CURSOR_MAGIC, CURSOR_SHAPE_BYTES, CURSOR_SHAPE_MAX, CURSOR_SHAPE_OFFSET, diff --git a/crates/pf-capture/src/windows/idd_push/cursor_blend.rs b/crates/pf-capture/src/windows/idd_push/cursor_blend.rs index cf389496..da55bd2c 100644 --- a/crates/pf-capture/src/windows/idd_push/cursor_blend.rs +++ b/crates/pf-capture/src/windows/idd_push/cursor_blend.rs @@ -10,9 +10,6 @@ //! alpha-blended quad (the GDI poller's full-fidelity shape at its polled position), entirely //! GPU-side on the capture device, before the normal conversion runs from the scratch. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::*; use windows::core::s; use windows::Win32::Graphics::Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST; diff --git a/crates/pf-capture/src/windows/idd_push/cursor_poll.rs b/crates/pf-capture/src/windows/idd_push/cursor_poll.rs index d3dc7f49..3dd7cc23 100644 --- a/crates/pf-capture/src/windows/idd_push/cursor_poll.rs +++ b/crates/pf-capture/src/windows/idd_push/cursor_poll.rs @@ -20,9 +20,6 @@ //! `winsta0\default` (the service supervisor retargets the token — `windows/service.rs` //! `spawn_host`), so the poller thread sees the session's cursor directly; no helper process. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::*; use windows::Win32::Graphics::Gdi::{ DeleteObject, GetDC, GetDIBits, GetObjectW, ReleaseDC, BITMAP, BITMAPINFO, BITMAPINFOHEADER, diff --git a/crates/pf-capture/src/windows/idd_push/descriptor.rs b/crates/pf-capture/src/windows/idd_push/descriptor.rs index 3866ba0c..c5d64e11 100644 --- a/crates/pf-capture/src/windows/idd_push/descriptor.rs +++ b/crates/pf-capture/src/windows/idd_push/descriptor.rs @@ -1,9 +1,6 @@ //! Off-thread display-descriptor polling (plan §W4, carved out of the IDD-push capturer): the //! live HDR state + active resolution of the virtual target, sampled off the capture loop via CCD. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::*; /// The display descriptor the capture loop follows: live HDR state + active resolution of the diff --git a/crates/pf-capture/src/windows/idd_push/dxgkrnl_etw.rs b/crates/pf-capture/src/windows/idd_push/dxgkrnl_etw.rs index 2b4f03e9..e9c59917 100644 --- a/crates/pf-capture/src/windows/idd_push/dxgkrnl_etw.rs +++ b/crates/pf-capture/src/windows/idd_push/dxgkrnl_etw.rs @@ -33,9 +33,6 @@ //! The session's `FlushTimer` is 1 s, so a bracket from the trailing second of a gap can land //! AFTER that stall's report line — the next report (and the metronomic tally) still carries it. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use std::collections::VecDeque; use std::sync::{Arc, Mutex, OnceLock, Weak}; use std::time::{Duration, Instant}; diff --git a/crates/pf-capture/src/windows/idd_push/probes.rs b/crates/pf-capture/src/windows/idd_push/probes.rs index d87a03e9..39941e3a 100644 --- a/crates/pf-capture/src/windows/idd_push/probes.rs +++ b/crates/pf-capture/src/windows/idd_push/probes.rs @@ -25,9 +25,6 @@ //! ([`acquire`]), refcounted across parallel capturers; probes sample at 20 Hz or slower and cost //! microseconds each, so the engine is invisible next to a streaming session. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use std::collections::VecDeque; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, Weak}; diff --git a/crates/pf-capture/src/windows/idd_push/stall.rs b/crates/pf-capture/src/windows/idd_push/stall.rs index 9ed9c735..27d729fc 100644 --- a/crates/pf-capture/src/windows/idd_push/stall.rs +++ b/crates/pf-capture/src/windows/idd_push/stall.rs @@ -1,9 +1,6 @@ //! Capture-stall detection (plan §W4, carved out of the IDD-push capturer): flags multi-hundred-ms //! holes in DWM frame delivery that open while the desktop was actively composing. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::*; /// A detected capture stall: a multi-hundred-ms hole in DWM's frame delivery that opened while the diff --git a/crates/pf-client-core/src/lib.rs b/crates/pf-client-core/src/lib.rs index 2c985143..b84c6ba7 100644 --- a/crates/pf-client-core/src/lib.rs +++ b/crates/pf-client-core/src/lib.rs @@ -18,7 +18,6 @@ // 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(any(target_os = "linux", windows))] mod au_dump; diff --git a/crates/pf-clipboard/src/host/windows.rs b/crates/pf-clipboard/src/host/windows.rs index 2af936a2..43dfc54a 100644 --- a/crates/pf-clipboard/src/host/windows.rs +++ b/crates/pf-clipboard/src/host/windows.rs @@ -17,8 +17,8 @@ //! (`PostMessage` is the documented thread-safe way to poke a message loop). Per-window state hangs //! off `GWLP_USERDATA`, so multiple concurrent sessions each get their own window + state. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] +// Every `unsafe` block in this file carries a `// SAFETY:` proof; the deny enforcing it sits at +// the crate root (lib.rs), covering every backend. use std::cell::RefCell; use std::sync::{Arc, Mutex}; diff --git a/crates/pf-clipboard/src/lib.rs b/crates/pf-clipboard/src/lib.rs index d035fca4..cb1e271c 100644 --- a/crates/pf-clipboard/src/lib.rs +++ b/crates/pf-clipboard/src/lib.rs @@ -10,6 +10,10 @@ //! [`spawn_decline_loop`] — so its control loop compiles unchanged on every host platform; the //! platform split lives entirely behind [`start`]. +// Unsafe-proof program: every `unsafe` block in any backend carries a `// SAFETY:` proof, +// enforced workspace-wide by `[workspace.lints]` — a new backend under `host/` is covered on +// creation. + use std::sync::atomic::AtomicBool; use std::sync::Arc; diff --git a/crates/pf-console-ui/src/lib.rs b/crates/pf-console-ui/src/lib.rs index 7e5a15f2..560a6557 100644 --- a/crates/pf-console-ui/src/lib.rs +++ b/crates/pf-console-ui/src/lib.rs @@ -11,7 +11,6 @@ //! capture hint, start banner. // Unsafe-proof program: every `unsafe {}` in the Skia/Vulkan overlay carries a `// SAFETY:` proof. -#![deny(clippy::undocumented_unsafe_blocks)] #[cfg(any(target_os = "linux", windows))] mod anim; diff --git a/crates/pf-dxvadec/src/lib.rs b/crates/pf-dxvadec/src/lib.rs index 88549a0e..841a3718 100644 --- a/crates/pf-dxvadec/src/lib.rs +++ b/crates/pf-dxvadec/src/lib.rs @@ -52,7 +52,6 @@ //! ([`dxva::as_bytes`] / [`dxva::slice_bytes`]), fenced behind a sealed trait //! that only this crate's `#[repr(C)]` PODs implement, and carrying a written //! proof — enforced: -#![deny(clippy::undocumented_unsafe_blocks)] pub mod config; pub mod descriptors; diff --git a/crates/pf-encode/src/enc/libav.rs b/crates/pf-encode/src/enc/libav.rs index 1954aa6e..35acb531 100644 --- a/crates/pf-encode/src/enc/libav.rs +++ b/crates/pf-encode/src/enc/libav.rs @@ -48,6 +48,8 @@ impl AvBuffer { /// allocator returns on failure (so the `is_null` check every caller used to open-code happens /// once, here). /// + // unsafe-fn-no-op-ok: contract-deferring constructor (`Vec::set_len` shape) — the body is + // safe; the ownership transfer promised here is what Drop/as_ptr later rely on. /// # Safety /// `p` must be null, or a live `AVBufferRef` whose ownership passes to the returned value — /// nothing else may unref it. diff --git a/crates/pf-encode/src/enc/linux/mod.rs b/crates/pf-encode/src/enc/linux/mod.rs index 9149df81..21638b26 100644 --- a/crates/pf-encode/src/enc/linux/mod.rs +++ b/crates/pf-encode/src/enc/linux/mod.rs @@ -12,8 +12,6 @@ //! does *not* accept — we expand it to `rgb0` (one padding byte/pixel, no colour math). //! The encoder is opened *without* a global header so VPS/SPS/PPS are emitted in-band on //! every IDR — the output is both a playable raw Annex-B stream and self-contained AUs. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] use super::{ChromaFormat, Codec, EncodedFrame, Encoder}; use anyhow::{anyhow, bail, Context, Result}; diff --git a/crates/pf-encode/src/enc/linux/nvenc_cuda.rs b/crates/pf-encode/src/enc/linux/nvenc_cuda.rs index 25bb1cc1..a8999802 100644 --- a/crates/pf-encode/src/enc/linux/nvenc_cuda.rs +++ b/crates/pf-encode/src/enc/linux/nvenc_cuda.rs @@ -63,8 +63,6 @@ // the signature. Clearing this file means DELETING the markers that carry no caller contract, not // wrapping the calls — until then the lint is off HERE and enforced everywhere else. #![allow(unsafe_op_in_unsafe_fn)] -// Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] use super::nvenc_core::{ apply_low_latency_config, build_init_params, cached_ceiling, cached_split_verdict, codec_guid, diff --git a/crates/pf-encode/src/enc/linux/vaapi.rs b/crates/pf-encode/src/enc/linux/vaapi.rs index d11086a6..ea5bd6c7 100644 --- a/crates/pf-encode/src/enc/linux/vaapi.rs +++ b/crates/pf-encode/src/enc/linux/vaapi.rs @@ -19,8 +19,6 @@ //! hwdevice/hwframes/buffersrc/buffersink calls go through `ffmpeg::ffi` (= `ffmpeg_sys_next`), //! as the CUDA encode path and the clients' decode paths already do. The encoder is opened //! *without* a global header, so VPS/SPS/PPS are in-band on every IDR. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] use super::{Codec, EncodedFrame, Encoder}; use anyhow::{anyhow, bail, Context, Result}; diff --git a/crates/pf-encode/src/enc/linux/worker.rs b/crates/pf-encode/src/enc/linux/worker.rs index 14ce0f31..f76f3fc4 100644 --- a/crates/pf-encode/src/enc/linux/worker.rs +++ b/crates/pf-encode/src/enc/linux/worker.rs @@ -41,9 +41,6 @@ //! worker caches it, so the steady state passes **zero** descriptors (the PipeWire pool recycles a //! small buffer set). -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use anyhow::{Context, Result}; use pf_frame::{CapturedFrame, CursorOverlay, DmabufFrame, FramePayload, PixelFormat}; use pf_zerocopy::ipc; diff --git a/crates/pf-encode/src/enc/nvenc_core.rs b/crates/pf-encode/src/enc/nvenc_core.rs index 242230f9..c59946bc 100644 --- a/crates/pf-encode/src/enc/nvenc_core.rs +++ b/crates/pf-encode/src/enc/nvenc_core.rs @@ -5,12 +5,16 @@ //! `libloading`), the device binding (D3D11 vs CUDA), input-surface registration, and the //! Windows-only async retrieve — stay in their backends. Sibling of [`super::nvenc_status`]. -// UNSAFE-LINT EXEMPTION (rationale + exit criteria: `unsafe_op_in_unsafe_fn` in the workspace -// Cargo.toml). This body is raw `nvEncodeAPI` entry-table calls almost line for line; narrowing it -// would add one `unsafe {}` plus one SAFETY comment per call that could only restate the signature. -// Clearing this file means DELETING the markers that carry no caller contract, not wrapping the -// calls — until then the lint is off HERE and enforced everywhere else. -#![allow(unsafe_op_in_unsafe_fn)] +// UNSAFE-LINT EXEMPTION REMOVED — the old fence rationale ("raw nvEncodeAPI entry-table calls +// almost line for line") was false for this file: it makes ZERO FFI calls. Its unsafe surface is +// C-union access whose soundness hangs entirely on which codec arm is active, and the 4:4:4 note +// below records the shipped bug (hevcConfig bytes stamped onto an AV1 config) that per-operation +// visibility makes findable. So this file runs the strictest discipline in the crate: every +// union READ, borrow, or bitfield-setter call sits in its own `unsafe {}` block naming the codec +// guard it relies on. (Plain union-arm field WRITES are safe by language rule — writing an arm +// cannot itself be UB; the hazard is the mismatched read — so those stay bare, guarded by the +// same codec matches.) +#![deny(clippy::multiple_unsafe_ops_per_block)] use super::Codec; use nvidia_video_codec_sdk::sys::nvEncodeAPI as nv; @@ -694,10 +698,9 @@ mod tests { }; assert_eq!(cfg.profileGUID, nv::NV_ENC_HEVC_PROFILE_FREXT_GUID); // SAFETY: an HEVC session's union arm is `hevcConfig` — the one this path wrote. - unsafe { - assert_eq!(cfg.encodeCodecConfig.hevcConfig.chromaFormatIDC(), 3); - assert_eq!(cfg.encodeCodecConfig.hevcConfig.pixelBitDepthMinus8(), 2); - } + unsafe { assert_eq!(cfg.encodeCodecConfig.hevcConfig.chromaFormatIDC(), 3) }; + // SAFETY: same HEVC arm as above. + unsafe { assert_eq!(cfg.encodeCodecConfig.hevcConfig.pixelBitDepthMinus8(), 2) }; } #[test] @@ -1210,6 +1213,8 @@ pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: Lo // are the only accepted config). H.264 has no tier. Level 0 = autoselect for HEVC. match c.codec { Codec::H265 => { + // Plain union-arm writes are safe by language rule (the hazard is a mismatched + // READ later); the match on `c.codec` keeps the arm honest. cfg.encodeCodecConfig.hevcConfig.tier = 1; cfg.encodeCodecConfig.hevcConfig.level = 0; } @@ -1264,21 +1269,29 @@ pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: Lo } if want_444 && c.codec == Codec::H265 { cfg.profileGUID = nv::NV_ENC_HEVC_PROFILE_FREXT_GUID; - cfg.encodeCodecConfig.hevcConfig.set_chromaFormatIDC(3); + // SAFETY: HEVC session (guarded by `c.codec == Codec::H265` on this branch), so + // `hevcConfig` is the active arm. + unsafe { cfg.encodeCodecConfig.hevcConfig.set_chromaFormatIDC(3) }; if c.bit_depth == 10 { - cfg.encodeCodecConfig.hevcConfig.set_pixelBitDepthMinus8(2); // Main 4:4:4 10 + // SAFETY: same HEVC arm, same branch guard. (Main 4:4:4 10) + unsafe { cfg.encodeCodecConfig.hevcConfig.set_pixelBitDepthMinus8(2) }; } } else if c.bit_depth == 10 { match c.codec { Codec::H265 => { cfg.profileGUID = nv::NV_ENC_HEVC_PROFILE_MAIN10_GUID; - cfg.encodeCodecConfig.hevcConfig.set_pixelBitDepthMinus8(2); + // SAFETY: HEVC session (matched on `c.codec`), so `hevcConfig` is the active arm. + unsafe { cfg.encodeCodecConfig.hevcConfig.set_pixelBitDepthMinus8(2) }; } Codec::Av1 => { - cfg.encodeCodecConfig.av1Config.set_pixelBitDepthMinus8(2); - cfg.encodeCodecConfig - .av1Config - .set_inputPixelBitDepthMinus8(c.av1_input_depth_minus8); + // SAFETY: AV1 session (matched on `c.codec`), so `av1Config` is the active arm. + unsafe { cfg.encodeCodecConfig.av1Config.set_pixelBitDepthMinus8(2) }; + // SAFETY: same AV1 arm, same match guard. + unsafe { + cfg.encodeCodecConfig + .av1Config + .set_inputPixelBitDepthMinus8(c.av1_input_depth_minus8) + }; } Codec::H264 => {} // no 10-bit H.264 encode on NVENC — negotiation never asks Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"), @@ -1306,7 +1319,9 @@ pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: Lo }; match c.codec { Codec::H265 => { - let vui = &mut cfg.encodeCodecConfig.hevcConfig.hevcVUIParameters; + // SAFETY: HEVC session (matched on `c.codec`), so `hevcConfig` is the active + // arm; the borrow is dropped before any other union access. + let vui = unsafe { &mut cfg.encodeCodecConfig.hevcConfig.hevcVUIParameters }; vui.videoSignalTypePresentFlag = 1; vui.videoFullRangeFlag = 0; vui.colourDescriptionPresentFlag = 1; @@ -1315,7 +1330,9 @@ pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: Lo vui.colourMatrix = mat; } Codec::H264 => { - let vui = &mut cfg.encodeCodecConfig.h264Config.h264VUIParameters; + // SAFETY: H.264 session (matched on `c.codec`), so `h264Config` is the active + // arm; the borrow is dropped before any other union access. + let vui = unsafe { &mut cfg.encodeCodecConfig.h264Config.h264VUIParameters }; vui.videoSignalTypePresentFlag = 1; vui.videoFullRangeFlag = 0; vui.colourDescriptionPresentFlag = 1; @@ -1324,7 +1341,9 @@ pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: Lo vui.colourMatrix = mat; } Codec::Av1 => { - let av1 = &mut cfg.encodeCodecConfig.av1Config; + // SAFETY: AV1 session (matched on `c.codec`), so `av1Config` is the active arm; + // the borrow is dropped before any other union access. + let av1 = unsafe { &mut cfg.encodeCodecConfig.av1Config }; av1.colorPrimaries = prim; av1.transferCharacteristics = trc; av1.matrixCoefficients = mat; diff --git a/crates/pf-encode/src/enc/sw.rs b/crates/pf-encode/src/enc/sw.rs index 732aa637..053790a5 100644 --- a/crates/pf-encode/src/enc/sw.rs +++ b/crates/pf-encode/src/enc/sw.rs @@ -12,8 +12,6 @@ //! defaulting to BT.709 limited — true of every punktfunk client (`csc_rows` falls back to 709 on //! "unspecified"), but NOT of vendor TV decoders, which guess colorimetry from RESOLUTION: an LG //! webOS panel reads a 4K SDR stream as BT.2020 and renders it visibly washed out. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] use super::{EncodedFrame, Encoder}; use anyhow::{bail, ensure, Context, Result}; diff --git a/crates/pf-encode/src/enc/windows/amf.rs b/crates/pf-encode/src/enc/windows/amf.rs index abc2a5e6..a11e2c56 100644 --- a/crates/pf-encode/src/enc/windows/amf.rs +++ b/crates/pf-encode/src/enc/windows/amf.rs @@ -49,8 +49,6 @@ // restate the signature. Clearing this file means DELETING the markers that carry no caller // contract, not wrapping the calls — until then the lint is off HERE and enforced everywhere else. #![allow(unsafe_op_in_unsafe_fn)] -// Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; use anyhow::{anyhow, bail, Context, Result}; diff --git a/crates/pf-encode/src/enc/windows/ffmpeg_win.rs b/crates/pf-encode/src/enc/windows/ffmpeg_win.rs index ed428523..c536b316 100644 --- a/crates/pf-encode/src/enc/windows/ffmpeg_win.rs +++ b/crates/pf-encode/src/enc/windows/ffmpeg_win.rs @@ -37,8 +37,6 @@ //! through `ffmpeg::ffi` (= `ffmpeg_sys_next`), exactly as the Linux CUDA/VAAPI paths do. The //! `AVD3D11VADeviceContext`/`AVD3D11VAFramesContext` layouts are mirrored (the bindings don't //! allowlist `hwcontext_d3d11va.h`), as [`super::linux`] mirrors `AVCUDADeviceContext`. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] use super::{ChromaFormat, Codec, EncodedFrame, Encoder}; use anyhow::{anyhow, bail, Context, Result}; diff --git a/crates/pf-encode/src/enc/windows/nvenc.rs b/crates/pf-encode/src/enc/windows/nvenc.rs index 67c0f7ce..f2269e25 100644 --- a/crates/pf-encode/src/enc/windows/nvenc.rs +++ b/crates/pf-encode/src/enc/windows/nvenc.rs @@ -39,8 +39,6 @@ // the signature. Clearing this file means DELETING the markers that carry no caller contract, not // wrapping the calls — until then the lint is off HERE and enforced everywhere else. #![allow(unsafe_op_in_unsafe_fn)] -// Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] use super::nvenc_core::{ apply_low_latency_config, build_init_params, cached_ceiling, codec_guid, plan_range_recovery, diff --git a/crates/pf-encode/src/enc/windows/qsv.rs b/crates/pf-encode/src/enc/windows/qsv.rs index 1912fd11..d2448dfe 100644 --- a/crates/pf-encode/src/enc/windows/qsv.rs +++ b/crates/pf-encode/src/enc/windows/qsv.rs @@ -37,9 +37,6 @@ //! it stays behind the same gate and falls back to IDR wherever the driver declines. 4:4:4 stays //! `false` until probed on real hardware (design §8.6). -// Every `unsafe` block / impl in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] - use super::{ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps}; use anyhow::{anyhow, bail, Context, Result}; use libvpl_sys as vpl; diff --git a/crates/pf-encode/src/lib.rs b/crates/pf-encode/src/lib.rs index 8c093b5c..918aefb8 100644 --- a/crates/pf-encode/src/lib.rs +++ b/crates/pf-encode/src/lib.rs @@ -12,7 +12,6 @@ // `#[cfg(test)]` instead. // Every unsafe block in this module tree carries a `// SAFETY:` proof; enforce it (unsafe-proof // program). As a parent module this also covers the child modules (windows/linux backends). -#![deny(clippy::undocumented_unsafe_blocks)] use anyhow::Result; use pf_frame::{CapturedFrame, PixelFormat}; diff --git a/crates/pf-frame/src/dxgi.rs b/crates/pf-frame/src/dxgi.rs index 609e1312..e846e3c0 100644 --- a/crates/pf-frame/src/dxgi.rs +++ b/crates/pf-frame/src/dxgi.rs @@ -7,9 +7,6 @@ //! The win32u GPU-preference hook, the HDR/video-engine converters, and the self-tests stay in the //! capture crate — they are capture mechanics, not shared identity. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use anyhow::{Context, Result}; use windows::core::Interface; use windows::Win32::Foundation::{HMODULE, LUID}; diff --git a/crates/pf-frame/src/lib.rs b/crates/pf-frame/src/lib.rs index ab946b8d..38cc96f7 100644 --- a/crates/pf-frame/src/lib.rs +++ b/crates/pf-frame/src/lib.rs @@ -10,7 +10,6 @@ //! tuning), and — on Windows — [`dxgi`] (the capture identity + D3D11 device creation). // Unsafe-proof program: every `unsafe {}` / `unsafe impl` must carry a `// SAFETY:` proof. -#![deny(clippy::undocumented_unsafe_blocks)] pub mod hdr; pub mod metronome; diff --git a/crates/pf-frame/src/session_tuning.rs b/crates/pf-frame/src/session_tuning.rs index 0e6700a6..0695fcbc 100644 --- a/crates/pf-frame/src/session_tuning.rs +++ b/crates/pf-frame/src/session_tuning.rs @@ -11,9 +11,6 @@ //! state) auto-revert at thread exit (= session end); the process-wide bits revert at process exit. //! See `design/host-latency-plan.md` Tier 3A. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - #[cfg(target_os = "windows")] mod imp { #![allow(non_snake_case)] diff --git a/crates/pf-frame/src/thread_qos.rs b/crates/pf-frame/src/thread_qos.rs index bc7ba85c..80a748bb 100644 --- a/crates/pf-frame/src/thread_qos.rs +++ b/crates/pf-frame/src/thread_qos.rs @@ -3,9 +3,6 @@ //! can't deschedule them; the native, GameStream, and direct-NVENC send threads all reach this the //! same way (`pf_frame::thread_qos::boost_thread_priority`). -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - /// Raise the current thread's OS scheduling priority so a CPU-heavy game can't deschedule our /// capture/encode/send threads. This matters even though our GPU work is already HIGH priority: the /// GPU scheduler can only favour commands we've actually SUBMITTED, so if a normal-priority thread is diff --git a/crates/pf-gpu/src/lib.rs b/crates/pf-gpu/src/lib.rs index 5364795b..241b38d2 100644 --- a/crates/pf-gpu/src/lib.rs +++ b/crates/pf-gpu/src/lib.rs @@ -23,7 +23,6 @@ //! live session actually encodes on, for the console's "in use" display. // Unsafe-proof program: every `unsafe {}` in this leaf carries a `// SAFETY:` proof. -#![deny(clippy::undocumented_unsafe_blocks)] use anyhow::Result; use serde::{Deserialize, Serialize}; diff --git a/crates/pf-inject/src/inject/linux/gamepad.rs b/crates/pf-inject/src/inject/linux/gamepad.rs index 0f557a4c..4250c15e 100644 --- a/crates/pf-inject/src/inject/linux/gamepad.rs +++ b/crates/pf-inject/src/inject/linux/gamepad.rs @@ -15,9 +15,6 @@ //! `` on x86_64. `/dev/uinput` needs a udev rule + `input` group membership //! (see `scripts/60-punktfunk.rules`); creation fails with a clear error otherwise. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use crate::pad_slots::PadSlots; use anyhow::{bail, Result}; use punktfunk_core::input::{gamepad, GamepadFrame, MAX_PADS}; diff --git a/crates/pf-inject/src/inject/linux/kwin_fake_input.rs b/crates/pf-inject/src/inject/linux/kwin_fake_input.rs index 2d577692..5c8e9efc 100644 --- a/crates/pf-inject/src/inject/linux/kwin_fake_input.rs +++ b/crates/pf-inject/src/inject/linux/kwin_fake_input.rs @@ -17,8 +17,6 @@ //! output's logical rectangle — the same shape the libei backend uses with its EI region. #![allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)] -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] use super::{gs_button_to_evdev, vk_to_evdev, InputEvent, InputInjector}; use anyhow::{Context, Result}; diff --git a/crates/pf-inject/src/inject/linux/wlr.rs b/crates/pf-inject/src/inject/linux/wlr.rs index c506e5a8..34bf2c76 100644 --- a/crates/pf-inject/src/inject/linux/wlr.rs +++ b/crates/pf-inject/src/inject/linux/wlr.rs @@ -6,9 +6,6 @@ //! to evdev/US), and translate events into virtual pointer/keyboard requests, tracking modifier //! state so the compositor resolves shifted keysyms correctly. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::{gs_button_to_evdev, vk_to_evdev, InputEvent, InputInjector}; use anyhow::{bail, Context, Result}; use punktfunk_core::input::InputKind; diff --git a/crates/pf-inject/src/inject/windows/pointer_windows.rs b/crates/pf-inject/src/inject/windows/pointer_windows.rs index f6664e24..109c70b4 100644 --- a/crates/pf-inject/src/inject/windows/pointer_windows.rs +++ b/crates/pf-inject/src/inject/windows/pointer_windows.rs @@ -15,9 +15,6 @@ //! with its position (never at a stale point), tip edges get their own DOWN/UP frames, and a //! range-leave is a final frame without `INRANGE`. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] - use anyhow::{Context, Result}; use punktfunk_core::input::{InputEvent, InputKind}; use punktfunk_core::quic::{ diff --git a/crates/pf-inject/src/inject/windows/sendinput.rs b/crates/pf-inject/src/inject/windows/sendinput.rs index 4f1baab9..93247118 100644 --- a/crates/pf-inject/src/inject/windows/sendinput.rs +++ b/crates/pf-inject/src/inject/windows/sendinput.rs @@ -14,9 +14,6 @@ //! user's, and any layout re-reads a *position* as a *character* — on a German host that is //! exactly the y↔z swap / ü-on-ö scramble. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] - use anyhow::Result; use punktfunk_core::input::{InputEvent, InputKind}; use std::mem::size_of; diff --git a/crates/pf-inject/src/lib.rs b/crates/pf-inject/src/lib.rs index dd1b3eb0..71bf4957 100644 --- a/crates/pf-inject/src/lib.rs +++ b/crates/pf-inject/src/lib.rs @@ -14,13 +14,6 @@ // Scaffold: trait methods + per-OS backends are defined ahead of the target that uses them. #![allow(dead_code)] -// Every unsafe block in this crate carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] -// …and its companion: without this, an `unsafe fn` body needs no blocks, so an unproven FFI call -// could hide inside one and still satisfy the deny above. The workspace keeps -// `unsafe_op_in_unsafe_fn` at `warn` while the encoder backends are cleared; this crate is at zero. -#![deny(unsafe_op_in_unsafe_fn)] - use anyhow::Result; use punktfunk_core::input::{InputEvent, InputKind}; diff --git a/crates/pf-presenter/src/lib.rs b/crates/pf-presenter/src/lib.rs index c868e158..431d3b5f 100644 --- a/crates/pf-presenter/src/lib.rs +++ b/crates/pf-presenter/src/lib.rs @@ -17,7 +17,6 @@ //! the decode chain there is Vulkan → D3D11VA → software. // Unsafe-proof program: every `unsafe {}` in this crate carries a `// SAFETY:` proof. -#![deny(clippy::undocumented_unsafe_blocks)] // THE VULKAN CONTRACT, stated once - most `// SAFETY:` proofs in this crate are an instance of it. // diff --git a/crates/pf-update/Cargo.toml b/crates/pf-update/Cargo.toml index 26ba05cc..051fa8b8 100644 --- a/crates/pf-update/Cargo.toml +++ b/crates/pf-update/Cargo.toml @@ -18,3 +18,6 @@ path = "src/main.rs" [target.'cfg(target_os = "linux")'.dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" + +[lints] +workspace = true diff --git a/crates/pf-vdisplay/src/lib.rs b/crates/pf-vdisplay/src/lib.rs index 319aa59c..f27baa20 100644 --- a/crates/pf-vdisplay/src/lib.rs +++ b/crates/pf-vdisplay/src/lib.rs @@ -39,13 +39,6 @@ // honest. (Was a bare crate-wide allow whose "scaffold, defined ahead of the target that uses them" // rationale had stopped being true.) #![cfg_attr(not(target_os = "linux"), allow(dead_code))] -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] -// …and that program only covers a whole `unsafe fn` body once the body needs its own block: in -// edition 2021 `unsafe_op_in_unsafe_fn` is allow-by-default, which exempted this crate's hardest -// FFI from the deny above — every IOCTL wrapper, and `restore_displays_ccd`, the call the whole -// Windows teardown path depends on to give the operator their physical panels back. -#![deny(unsafe_op_in_unsafe_fn)] use anyhow::Result; pub use punktfunk_core::Mode; diff --git a/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs b/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs index b75e98bf..0eab03a4 100644 --- a/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs +++ b/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs @@ -23,9 +23,6 @@ //! "Could not find output". We talk raw Wayland on `$WAYLAND_DISPLAY`, so the host must run inside //! the KWin session's environment. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::{Mode, VirtualDisplay, VirtualOutput}; use anyhow::{anyhow, bail, Context, Result}; use std::os::fd::{AsFd, AsRawFd}; diff --git a/crates/pf-vdisplay/src/vdisplay/linux/kwin_output_mgmt.rs b/crates/pf-vdisplay/src/vdisplay/linux/kwin_output_mgmt.rs index 2a219a17..92b84b5f 100644 --- a/crates/pf-vdisplay/src/vdisplay/linux/kwin_output_mgmt.rs +++ b/crates/pf-vdisplay/src/vdisplay/linux/kwin_output_mgmt.rs @@ -20,8 +20,6 @@ //! each output's name / enabled / priority / current-mode size, then build a //! `kde_output_configuration_v2` and `apply()` it, waiting for `applied` / `failed`. -#![deny(clippy::undocumented_unsafe_blocks)] - use std::collections::HashMap; use std::os::fd::{AsFd, AsRawFd}; use std::time::{Duration, Instant}; diff --git a/crates/pf-vdisplay/src/vdisplay/windows/manager.rs b/crates/pf-vdisplay/src/vdisplay/windows/manager.rs index d49a4f50..049da9da 100644 --- a/crates/pf-vdisplay/src/vdisplay/windows/manager.rs +++ b/crates/pf-vdisplay/src/vdisplay/windows/manager.rs @@ -14,9 +14,6 @@ //! its `Drop` releases the refcount (a *stale* lease — its monitor was preempted + recreated under it — //! is a no-op, so it can never tear down the live monitor). -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use std::collections::BTreeMap; use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; diff --git a/crates/pf-vdisplay/src/vdisplay/windows/manager/driver.rs b/crates/pf-vdisplay/src/vdisplay/windows/manager/driver.rs index 510ad56d..4a7a287a 100644 --- a/crates/pf-vdisplay/src/vdisplay/windows/manager/driver.rs +++ b/crates/pf-vdisplay/src/vdisplay/windows/manager/driver.rs @@ -81,6 +81,8 @@ pub(crate) trait VdisplayDriver: Send + Sync { /// The monitor is NOT departed; the caller CCD-forces the freshly-advertised mode afterwards. /// The default errs so a backend without support routes to the re-arrival fallback. /// + // unsafe-fn-no-op-ok: trait method — the "dev is live" contract binds every impl; this + // default body is a stub that bails. /// # Safety /// `dev` must be the live control handle. unsafe fn update_modes(&self, dev: HANDLE, key: &MonitorKey, mode: Mode) -> Result<()> { @@ -114,6 +116,7 @@ mod tests { fn open(&self, _reap_orphans: bool) -> Result<(OwnedHandle, u32, u32)> { anyhow::bail!("fake driver has no control device") } + // unsafe-fn-no-op-ok: signature mandated by the trait; test stub. unsafe fn add_monitor( &self, _dev: HANDLE, @@ -125,9 +128,11 @@ mod tests { ) -> Result { anyhow::bail!("fake driver adds no monitors") } + // unsafe-fn-no-op-ok: signature mandated by the trait; test stub. unsafe fn remove_monitor(&self, _dev: HANDLE, _key: &MonitorKey) -> Result<()> { Ok(()) } + // unsafe-fn-no-op-ok: signature mandated by the trait; test stub. unsafe fn ping(&self, _dev: HANDLE) -> Result<()> { Ok(()) } diff --git a/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs b/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs index 355d3ee6..e91e8ae7 100644 --- a/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs +++ b/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs @@ -16,9 +16,6 @@ //! Only the driver-specific bits (GUID, IOCTL codes, request/reply structs, the version handshake) are //! here, per `pf_driver_proto`. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use std::ffi::c_void; use std::mem::size_of; use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}; diff --git a/crates/pf-vkdecode/src/lib.rs b/crates/pf-vkdecode/src/lib.rs index d46407ae..896f8f5a 100644 --- a/crates/pf-vkdecode/src/lib.rs +++ b/crates/pf-vkdecode/src/lib.rs @@ -112,10 +112,9 @@ //! Unsafe posture: unlike pf-bitstream (which forbids unsafe outright), this crate //! cannot — the `ash::vk::native` bindgen structs are zero-initialized the way the //! encode side does it (`pf-encode/src/enc/linux/vk_build.rs`), and the GPU half is -//! Vulkan FFI. Every unsafe block therefore carries a written `// SAFETY:` proof, -//! enforced (and unlike the encoder there is NO file-level -//! `unsafe_op_in_unsafe_fn` exemption — every operation is individually fenced): -#![deny(clippy::undocumented_unsafe_blocks)] +//! Vulkan FFI. Every unsafe block therefore carries a written `// SAFETY:` proof — enforced by +//! the workspace `[workspace.lints]` tables, and (unlike the encoder) with NO file-level +//! `unsafe_op_in_unsafe_fn` exemption: every operation is individually fenced. pub mod caps; pub mod caps_av1; diff --git a/crates/pf-vkdecode/tests/gpu_parity.rs b/crates/pf-vkdecode/tests/gpu_parity.rs index b1a03d3f..d4675f19 100644 --- a/crates/pf-vkdecode/tests/gpu_parity.rs +++ b/crates/pf-vkdecode/tests/gpu_parity.rs @@ -88,8 +88,6 @@ //! the readback geometry (row pitch / crop) or intra decode; mismatches that //! only appear on later frames point at inter prediction / DPB management. -#![deny(clippy::undocumented_unsafe_blocks)] - mod common; use ash::vk; diff --git a/crates/pf-vkdecode/tests/gpu_smoke.rs b/crates/pf-vkdecode/tests/gpu_smoke.rs index b7c4bba3..53efb01d 100644 --- a/crates/pf-vkdecode/tests/gpu_smoke.rs +++ b/crates/pf-vkdecode/tests/gpu_smoke.rs @@ -39,8 +39,6 @@ //! so releases pass `false`), soak, and both vendors' DPB arrangements at once //! (each box exercises only its own). -#![deny(clippy::undocumented_unsafe_blocks)] - mod common; use ash::vk; diff --git a/crates/pf-win-display/src/display_events.rs b/crates/pf-win-display/src/display_events.rs index 6dbe2d75..d0988703 100644 --- a/crates/pf-win-display/src/display_events.rs +++ b/crates/pf-win-display/src/display_events.rs @@ -28,9 +28,6 @@ //! suspects — without ever touching the CCD lock itself (the display-config lock is exactly what //! stalls during churn; the capture thread must never block on it). -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use std::collections::VecDeque; use std::sync::{Mutex, Once, OnceLock}; use std::time::Instant; diff --git a/crates/pf-win-display/src/lib.rs b/crates/pf-win-display/src/lib.rs index ab4a83a5..46a9b928 100644 --- a/crates/pf-win-display/src/lib.rs +++ b/crates/pf-win-display/src/lib.rs @@ -12,8 +12,6 @@ // `win_display` has denied both unsafe-proof lints since its CCD helpers stopped being `unsafe fn`; // hoist that to the crate root so the smaller modules (`input_desktop`, `monitor_devnode`, // `display_events`) and any future one are covered by default rather than by remembering to opt in. -#![deny(clippy::undocumented_unsafe_blocks)] -#![deny(unsafe_op_in_unsafe_fn)] #[cfg(target_os = "windows")] pub mod display_events; diff --git a/crates/pf-win-display/src/win_display.rs b/crates/pf-win-display/src/win_display.rs index e0b5b66d..80cbd8d7 100644 --- a/crates/pf-win-display/src/win_display.rs +++ b/crates/pf-win-display/src/win_display.rs @@ -8,13 +8,6 @@ //! them, which let the SudoVDA backend be dropped without losing them (audit §9 / Goal 2 — done). The //! plan's `windows/display_ccd.rs`. Extracted verbatim from the former SudoVDA backend before its removal. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] -// …and that program only covers a whole `unsafe fn` body once the body needs its own block: in -// edition 2021 `unsafe_op_in_unsafe_fn` is allow-by-default, which exempted every CCD/GDI helper -// below — including `restore_displays_ccd`, the call pf-vdisplay's teardown path depends on to give -// the operator their physical panels back. -#![deny(unsafe_op_in_unsafe_fn)] // The CCD/GDI helpers below are SAFE fns. They were `unsafe fn` for a decade of habit rather than a // memory-safety obligation: every one takes `Copy` scalars or borrowed Rust data, returns owned // values, and discharges its own FFI preconditions internally (`retry_set_display_config` even binds diff --git a/crates/pf-zerocopy/src/dmabuf_fence.rs b/crates/pf-zerocopy/src/dmabuf_fence.rs index 40d68ad0..48a7cff1 100644 --- a/crates/pf-zerocopy/src/dmabuf_fence.rs +++ b/crates/pf-zerocopy/src/dmabuf_fence.rs @@ -14,9 +14,6 @@ //! wait, no harm, and `WaitOutcome::NoFence` tells us the driver doesn't fence (so zero-copy //! would still race). -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use std::os::fd::RawFd; use std::time::{Duration, Instant}; diff --git a/crates/pf-zerocopy/src/imp/client.rs b/crates/pf-zerocopy/src/imp/client.rs index 563cb84d..8e63e9db 100644 --- a/crates/pf-zerocopy/src/imp/client.rs +++ b/crates/pf-zerocopy/src/imp/client.rs @@ -6,9 +6,6 @@ //! A worker death — the whole point of the isolation — surfaces as an `Err` with //! [`RemoteImporter::dead`] set, never as a host fault. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::cuda::{self, CUdeviceptr, DeviceBuffer, CU_IPC_HANDLE_SIZE}; use super::egl::DmabufPlane; use super::ipc; diff --git a/crates/pf-zerocopy/src/imp/cuda.rs b/crates/pf-zerocopy/src/imp/cuda.rs index bfb09f48..ddcea5a2 100644 --- a/crates/pf-zerocopy/src/imp/cuda.rs +++ b/crates/pf-zerocopy/src/imp/cuda.rs @@ -18,8 +18,6 @@ //! driver — see [`super::egl`].) #![allow(non_camel_case_types, non_snake_case)] -// Every `unsafe` block/impl below carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] use anyhow::{bail, Result}; use std::os::raw::{c_uint, c_void}; diff --git a/crates/pf-zerocopy/src/imp/cuda/ffi.rs b/crates/pf-zerocopy/src/imp/cuda/ffi.rs index f77d4932..7e8a18b9 100644 --- a/crates/pf-zerocopy/src/imp/cuda/ffi.rs +++ b/crates/pf-zerocopy/src/imp/cuda/ffi.rs @@ -5,8 +5,6 @@ //! and drive this layer. #![allow(non_camel_case_types, non_snake_case)] -// Every `unsafe` block/impl below carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] use anyhow::{bail, Result}; use std::os::raw::{c_int, c_uint, c_void}; diff --git a/crates/pf-zerocopy/src/imp/egl.rs b/crates/pf-zerocopy/src/imp/egl.rs index 9d6b1a81..a7036597 100644 --- a/crates/pf-zerocopy/src/imp/egl.rs +++ b/crates/pf-zerocopy/src/imp/egl.rs @@ -12,8 +12,6 @@ //! owned [`DeviceBuffer`] so the dmabuf can be returned to the compositor immediately. #![allow(non_upper_case_globals)] -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] use super::cuda::{self, DeviceBuffer}; use anyhow::{ensure, Context as _, Result}; diff --git a/crates/pf-zerocopy/src/imp/egl/gl.rs b/crates/pf-zerocopy/src/imp/egl/gl.rs index 32116f7c..b2062fdf 100644 --- a/crates/pf-zerocopy/src/imp/egl/gl.rs +++ b/crates/pf-zerocopy/src/imp/egl/gl.rs @@ -5,8 +5,6 @@ //! [`super`]. #![allow(non_upper_case_globals)] -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] use anyhow::{bail, ensure, Result}; use std::os::raw::{c_int, c_void}; diff --git a/crates/pf-zerocopy/src/imp/ipc.rs b/crates/pf-zerocopy/src/imp/ipc.rs index 89fc8de4..f94e0484 100644 --- a/crates/pf-zerocopy/src/imp/ipc.rs +++ b/crates/pf-zerocopy/src/imp/ipc.rs @@ -18,9 +18,6 @@ //! inode with `punktfunk-host`, because a shared inode shares the file capability — so it passes //! its own resolved path to [`spawn_worker`] instead. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use serde::de::DeserializeOwned; use serde::Serialize; use std::fs::File; diff --git a/crates/pf-zerocopy/src/imp/vkslot.rs b/crates/pf-zerocopy/src/imp/vkslot.rs index 9bf703da..674b87c5 100644 --- a/crates/pf-zerocopy/src/imp/vkslot.rs +++ b/crates/pf-zerocopy/src/imp/vkslot.rs @@ -34,9 +34,6 @@ //! Falls back cleanly: if bring-up fails the encoder allocates plain CUDA surfaces and composite //! mode degrades to no cursor (warned once) — never a failed session. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::cuda::{self, CUdeviceptr}; use anyhow::{anyhow, Context as _, Result}; use ash::vk; diff --git a/crates/pf-zerocopy/src/imp/vulkan.rs b/crates/pf-zerocopy/src/imp/vulkan.rs index 08157bc4..a7990424 100644 --- a/crates/pf-zerocopy/src/imp/vulkan.rs +++ b/crates/pf-zerocopy/src/imp/vulkan.rs @@ -16,9 +16,6 @@ //! a stream's life). Falls back cleanly: any init/import error disables the importer and the //! CPU mmap path takes over. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::cuda::{self, DeviceBuffer}; use anyhow::{anyhow, bail, Context as _, Result}; use ash::vk; diff --git a/crates/pf-zerocopy/src/imp/worker.rs b/crates/pf-zerocopy/src/imp/worker.rs index a439384c..45782c68 100644 --- a/crates/pf-zerocopy/src/imp/worker.rs +++ b/crates/pf-zerocopy/src/imp/worker.rs @@ -9,9 +9,6 @@ //! only happens after the capturer AND every in-flight frame on the host side are gone, so pooled //! device memory is never freed under a frame the host still reads. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use super::cuda::{self, CUdeviceptr, DeviceBuffer}; use super::egl::{DmabufPlane, EglImporter}; use super::ipc; diff --git a/crates/pf-zerocopy/src/lib.rs b/crates/pf-zerocopy/src/lib.rs index 8f6d1eee..f3f0c44a 100644 --- a/crates/pf-zerocopy/src/lib.rs +++ b/crates/pf-zerocopy/src/lib.rs @@ -8,13 +8,9 @@ //! consumes the shared frame vocabulary, which sits ABOVE this crate (this crate provides the //! `DeviceBuffer` that vocabulary's `FramePayload::Cuda` owns). -// Unsafe-proof program: every `unsafe {}` / `unsafe impl` must carry a `// SAFETY:` proof. Each -// file keeps its own `#![deny(...)]` too; this crate-root deny is the catch-all gate. -// `unsafe_op_in_unsafe_fn` closes the gap the clippy lint leaves: operations inside an -// `unsafe fn` body are not "unsafe blocks", so without it ~45 functions' worth of raw driver -// calls sat OUTSIDE the invariant this crate advertises. -#![deny(clippy::undocumented_unsafe_blocks)] -#![deny(unsafe_op_in_unsafe_fn)] +// Unsafe-proof program: every `unsafe {}` / `unsafe impl` carries a `// SAFETY:` proof, and +// `unsafe fn` bodies need explicit blocks (~45 functions' worth of raw driver calls used to sit +// outside that invariant). Both lints are enforced by the workspace `[workspace.lints]` tables. /// Wait for a dmabuf's implicit read-ready fence (`DMA_BUF_IOCTL_EXPORT_SYNC_FILE` + poll). #[cfg(target_os = "linux")] diff --git a/crates/punktfunk-core/src/abi.rs b/crates/punktfunk-core/src/abi.rs index d90a53c7..73f9f2f7 100644 --- a/crates/punktfunk-core/src/abi.rs +++ b/crates/punktfunk-core/src/abi.rs @@ -53,6 +53,19 @@ use std::os::raw::c_char; use std::panic::AssertUnwindSafe; use std::ptr; +/// Poison-recovering lock for the C ABI surface. `.lock().unwrap()` inside an `extern "C"` fn +/// turns a poisoned mutex (some other thread panicked mid-write) into a panic across the C +/// boundary — an abort since Rust 1.81, exactly the class the panic-in-extern grep gate exists +/// for. The slots behind these mutexes are plain last-value caches (frame/audio/cursor/clip), so +/// whatever a poisoned writer left behind is still structurally valid data to overwrite or hand +/// out; recovering the guard is strictly better than aborting the embedding application. +/// (`quic`-gated with its only callers, the `punktfunk_connection_*` entry points — a +/// `default-features = false` consumer like the tray would otherwise see dead code.) +#[cfg(feature = "quic")] +fn lock_recover(m: &std::sync::Mutex) -> std::sync::MutexGuard<'_, T> { + m.lock().unwrap_or_else(std::sync::PoisonError::into_inner) +} + /// Opaque session handle. Pointer-only from C. pub struct PunktfunkSession { inner: Session, @@ -471,8 +484,7 @@ pub unsafe extern "C" fn punktfunk_client_poll_frame( } match s.inner.poll_frame() { Ok(frame) => { - s.last_frame = Some(frame); - let f = s.last_frame.as_ref().unwrap(); + let f = s.last_frame.insert(frame); // SAFETY: per the ABI contract - `out` is a caller-owned writable slot of the // matching `#[repr(C)]` type, written once by value. unsafe { @@ -2249,9 +2261,8 @@ pub unsafe extern "C" fn punktfunk_connection_next_au( .next_frame(std::time::Duration::from_millis(timeout_ms as u64)) { Ok(frame) => { - let mut slot = c.last.lock().unwrap(); - *slot = Some(frame); - let f = slot.as_ref().unwrap(); + let mut slot = lock_recover(&c.last); + let f = slot.insert(frame); // SAFETY: per the ABI contract - `out` is a caller-owned writable slot of the // matching `#[repr(C)]` type, written once by value. unsafe { @@ -2314,9 +2325,8 @@ pub unsafe extern "C" fn punktfunk_connection_next_audio( .next_audio(std::time::Duration::from_millis(timeout_ms as u64)) { Ok(pkt) => { - let mut slot = c.last_audio.lock().unwrap(); - *slot = Some(pkt); - let p = slot.as_ref().unwrap(); + let mut slot = lock_recover(&c.last_audio); + let p = slot.insert(pkt); // SAFETY: per the ABI contract - `out` is a caller-owned writable slot of the // matching `#[repr(C)]` type, written once by value. unsafe { @@ -2467,7 +2477,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_audio_pcm( Ok(pkt) => pkt, Err(e) => return e.status(), }; - let mut state = c.audio_pcm.lock().unwrap(); + let mut state = lock_recover(&c.audio_pcm); match state.decode_packet(&pkt.data, pkt.seq, channels) { // Nothing to hand out this call: a DTX silence marker with no loss owed before it. Ok(0) => PunktfunkStatus::NoFrame, @@ -3072,9 +3082,8 @@ pub unsafe extern "C" fn punktfunk_connection_next_cursor_shape( .next_cursor_shape(std::time::Duration::from_millis(timeout_ms as u64)) { Ok(shape) => { - let mut slot = c.last_cursor_shape.lock().unwrap(); - *slot = Some(shape); - let sh = slot.as_ref().unwrap(); + let mut slot = lock_recover(&c.last_cursor_shape); + let sh = slot.insert(shape); // SAFETY: per the ABI contract - `out` is a caller-owned writable slot of the // matching `#[repr(C)]` type, written once by value. unsafe { @@ -4053,7 +4062,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_clipboard( .next_clip(std::time::Duration::from_millis(timeout_ms as u64)) { Ok(ev) => { - let mut slot = c.last_clip.lock().unwrap(); + let mut slot = lock_recover(&c.last_clip); let out_ev = build_clip_event(ev, &mut slot); // SAFETY: per the ABI contract - a caller-owned out-param, non-null on this path, // written once by value. @@ -4065,7 +4074,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_clipboard( // traffic is sporadic, so without this a one-off 50 MiB paste stays resident // for the rest of the session (there is no other release entry point). The // borrow contract already says `out` data is valid only until the next call. - *c.last_clip.lock().unwrap() = None; + *lock_recover(&c.last_clip) = None; e.status() } } diff --git a/crates/punktfunk-core/src/lib.rs b/crates/punktfunk-core/src/lib.rs index 42780f77..cfd6b7e5 100644 --- a/crates/punktfunk-core/src/lib.rs +++ b/crates/punktfunk-core/src/lib.rs @@ -46,7 +46,6 @@ // `qos_windows`) — sendmmsg/recvmsg_x/USO/qWAVE move caller-owned buffers, nothing more. // A new module parsing wire data may NOT add a carve-out. #![deny(unsafe_code)] -#![deny(clippy::undocumented_unsafe_blocks)] #![forbid(unsafe_op_in_unsafe_fn)] pub mod abi; diff --git a/crates/punktfunk-host/src/audio/windows/audio_control.rs b/crates/punktfunk-host/src/audio/windows/audio_control.rs index c0f24f5e..1f4f2be2 100644 --- a/crates/punktfunk-host/src/audio/windows/audio_control.rs +++ b/crates/punktfunk-host/src/audio/windows/audio_control.rs @@ -46,9 +46,6 @@ //! `PUNKTFUNK_KEEP_DEFAULT`) leaves the user's chosen defaults untouched — the plan is still //! computed, since the mic must still pick a target. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] - use super::wiring_plan::{self, plan, plan_with_formats, Endpoint, MixFormat, Wiring}; use anyhow::{anyhow, bail, Result}; use std::ffi::c_void; diff --git a/crates/punktfunk-host/src/audio/windows/audio_probe.rs b/crates/punktfunk-host/src/audio/windows/audio_probe.rs index b756c3b5..59356cd3 100644 --- a/crates/punktfunk-host/src/audio/windows/audio_probe.rs +++ b/crates/punktfunk-host/src/audio/windows/audio_probe.rs @@ -21,9 +21,6 @@ //! endpoint of that name, and the probe restores the default playback/recording devices it //! disturbed before exiting. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] - use super::pad_endpoint as pe; use super::{audio_control, SAMPLE_RATE}; use anyhow::{anyhow, bail, Context, Result}; diff --git a/crates/punktfunk-host/src/audio/windows/devnode_cleanup.rs b/crates/punktfunk-host/src/audio/windows/devnode_cleanup.rs index de36e8d4..97b5f070 100644 --- a/crates/punktfunk-host/src/audio/windows/devnode_cleanup.rs +++ b/crates/punktfunk-host/src/audio/windows/devnode_cleanup.rs @@ -24,9 +24,6 @@ //! bundled one all carry no marker and are therefore untouchable here — uninstalling punktfunk //! removes what punktfunk created, and nothing else. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] - use super::{audio_control, audio_probe, minted, pad_endpoint as pe}; use anyhow::Result; use windows::Win32::Devices::DeviceAndDriverInstallation::SetupDiEnumDeviceInfo; diff --git a/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs b/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs index 624ad95d..4bc65054 100644 --- a/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs +++ b/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs @@ -35,9 +35,6 @@ //! COM discipline matches the sibling modules: WASAPI/COM objects live on the thread that made //! them (the provisioning worker, the capture thread); only channels and plain data cross. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] - use super::{audio_control, AudioCapturer, SAMPLE_RATE}; use anyhow::{anyhow, bail, Context, Result}; use std::collections::{HashSet, VecDeque}; diff --git a/crates/punktfunk-host/src/audio/windows/wasapi_mic.rs b/crates/punktfunk-host/src/audio/windows/wasapi_mic.rs index 3339c27b..e3a091dd 100644 --- a/crates/punktfunk-host/src/audio/windows/wasapi_mic.rs +++ b/crates/punktfunk-host/src/audio/windows/wasapi_mic.rs @@ -28,9 +28,6 @@ //! ([`VirtualMic::set_target_depth`]), filling silence when the client isn't talking. WASAPI //! objects are `!Send`, so they live entirely on that thread (mirrors `WasapiLoopbackCapturer`). -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] - use super::{audio_control, MicBackendStats, VirtualMic, SAMPLE_RATE}; use anyhow::{anyhow, Context, Result}; use std::collections::VecDeque; diff --git a/crates/punktfunk-host/src/gamestream/audio.rs b/crates/punktfunk-host/src/gamestream/audio.rs index 2a2e1cff..8442453c 100644 --- a/crates/punktfunk-host/src/gamestream/audio.rs +++ b/crates/punktfunk-host/src/gamestream/audio.rs @@ -17,9 +17,6 @@ //! data packets are consumed immediately and missing parity only costs loss recovery — so //! the validated stereo path stays byte-identical (data packets only, exactly as before). -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] - #[cfg(any(target_os = "linux", target_os = "windows", test))] use crate::audio::SAMPLE_RATE; #[cfg(any(target_os = "linux", target_os = "windows"))] diff --git a/crates/punktfunk-host/src/gamestream/stream.rs b/crates/punktfunk-host/src/gamestream/stream.rs index 41f5d824..49c8ef3c 100644 --- a/crates/punktfunk-host/src/gamestream/stream.rs +++ b/crates/punktfunk-host/src/gamestream/stream.rs @@ -3,9 +3,6 @@ //! either real portal desktop capture (`PUNKTFUNK_VIDEO_SOURCE=portal`, the portal PipeWire path) or //! a synthetic test pattern (default). Runs on its own native thread. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it. -#![deny(clippy::undocumented_unsafe_blocks)] - use super::video::{FrameType, VideoPacketizer}; use super::VIDEO_PORT; use crate::capture::{self, Capturer, FastSyntheticCapturer}; diff --git a/crates/punktfunk-host/src/linux/drm_sync.rs b/crates/punktfunk-host/src/linux/drm_sync.rs index a5989d82..d3f9fe22 100644 --- a/crates/punktfunk-host/src/linux/drm_sync.rs +++ b/crates/punktfunk-host/src/linux/drm_sync.rs @@ -8,8 +8,6 @@ //! verified (ioctl numbers + a live signal→wait round trip), ready to wire in the moment a producer //! gains working `SPA_META_SyncTimeline`. #![allow(dead_code)] -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] //! //! Compositors that render directly into the PipeWire buffer pool (Mutter's virtual //! monitors) hand buffers over at GPU-submit time; on drivers without implicit dmabuf diff --git a/crates/punktfunk-host/src/linux/gpuclocks.rs b/crates/punktfunk-host/src/linux/gpuclocks.rs index 62c467da..acf7660c 100644 --- a/crates/punktfunk-host/src/linux/gpuclocks.rs +++ b/crates/punktfunk-host/src/linux/gpuclocks.rs @@ -37,8 +37,6 @@ //! self-heals. Deliberately //! NOT default-on: it defeats idle downclocking for the whole box and is wrong on //! battery-powered hosts. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] use std::os::raw::{c_char, c_int, c_uint, c_void}; use std::sync::{Mutex, OnceLock}; diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index 9c5b4484..60022dd2 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -13,16 +13,10 @@ // Scaffold: trait methods and config paths are defined ahead of their backends. #![allow(dead_code)] -// Unsafe-proof program: every `unsafe {}` / `unsafe impl` in the crate must carry a `// SAFETY:` -// proof of why it is sound. This crate-root deny is the permanent, catch-all gate (it also covers -// any future module); individual files keep their own `#![deny(...)]` as belt-and-suspenders. -#![deny(clippy::undocumented_unsafe_blocks)] -// The companion gate: a proof only covers what it is attached to, and an `unsafe fn` body without -// this lint needs no blocks at all — so an unproven FFI call could hide inside one and satisfy the -// deny above. The workspace sets `unsafe_op_in_unsafe_fn` to `warn` (a ratchet across ~590 sites); -// this crate is at zero, so it denies. Keep the marker only where a caller can actually violate +// Unsafe-proof program (both lints now enforced by the workspace `[workspace.lints]` tables): +// every `unsafe {}` / `unsafe impl` carries a `// SAFETY:` proof, and `unsafe fn` bodies need +// explicit blocks. Keep the `unsafe fn` marker only where a caller can actually violate // something — a raw pointer or a borrowed `HANDLE` parameter, as in `service::spawn_host`. -#![deny(unsafe_op_in_unsafe_fn)] mod audio; mod bringup; diff --git a/crates/punktfunk-host/src/native.rs b/crates/punktfunk-host/src/native.rs index 263e3bd6..f8d5b8c7 100644 --- a/crates/punktfunk-host/src/native.rs +++ b/crates/punktfunk-host/src/native.rs @@ -22,9 +22,6 @@ //! Trust: the host serves with its persistent identity (`~/.config/punktfunk/cert.pem`, shared //! with GameStream pairing) and logs the SHA-256 fingerprint clients pin. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use anyhow::{anyhow, Context, Result}; use punktfunk_core::config::{CompositorPref, FecConfig, FecScheme, GamepadPref, Role}; use punktfunk_core::input::{InputEvent, InputKind}; diff --git a/crates/punktfunk-host/src/windows/crash.rs b/crates/punktfunk-host/src/windows/crash.rs index 01a5c4f6..fb0e4885 100644 --- a/crates/punktfunk-host/src/windows/crash.rs +++ b/crates/punktfunk-host/src/windows/crash.rs @@ -9,7 +9,6 @@ //! diagnosis. The Rust-panic analogue (a panic hook that tees into `tracing`) lives in `main()`. // Every `unsafe` block in this file carries a `// SAFETY:` proof (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] use windows::Win32::Foundation::HMODULE; use windows::Win32::System::Diagnostics::Debug::{ diff --git a/crates/punktfunk-host/src/windows/interactive.rs b/crates/punktfunk-host/src/windows/interactive.rs index 719aebe4..1ffc1b51 100644 --- a/crates/punktfunk-host/src/windows/interactive.rs +++ b/crates/punktfunk-host/src/windows/interactive.rs @@ -14,13 +14,8 @@ //! that is correct for launching *our own* streamer, but a store launcher needs the real user's token //! for activation + auth). The host process itself stays SYSTEM. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] -// …and the proofs only cover the whole file once an `unsafe fn` body needs its own blocks: the -// workspace sets `unsafe_op_in_unsafe_fn` to `warn`, which is a ratchet, not a floor. This module is -// at zero, so hold it there — `merged_env_block`'s pointer walk is the one real contract here, and -// it must not silently re-absorb the FFI calls around it. -#![deny(unsafe_op_in_unsafe_fn)] +// This module is at zero `unsafe fn` markers; hold it there — `merged_env_block`'s pointer walk +// is the one real contract here, and it must not silently re-absorb the FFI calls around it. use anyhow::{bail, Context, Result}; use std::path::Path; diff --git a/crates/punktfunk-host/src/windows/service.rs b/crates/punktfunk-host/src/windows/service.rs index efdbb2da..8f0c1fab 100644 --- a/crates/punktfunk-host/src/windows/service.rs +++ b/crates/punktfunk-host/src/windows/service.rs @@ -25,9 +25,6 @@ //! loaded into the service's environment and carried to the host child. Logs land in //! `%ProgramData%\punktfunk\logs\`. -// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). -#![deny(clippy::undocumented_unsafe_blocks)] - use anyhow::{bail, Context, Result}; use std::ffi::{c_void, OsString}; use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}; diff --git a/crates/punktfunk-host/vendor/usbip-sim/Cargo.toml b/crates/punktfunk-host/vendor/usbip-sim/Cargo.toml index 0f1a06a9..fe572326 100644 --- a/crates/punktfunk-host/vendor/usbip-sim/Cargo.toml +++ b/crates/punktfunk-host/vendor/usbip-sim/Cargo.toml @@ -1,3 +1,7 @@ +# Vendored snapshot — like pf-bitstream/vendor/cros-codecs, deliberately NOT opted into the +# workspace [lints] tables: upstream code stays as close to pristine as the trim allows, so a +# re-sync against upstream stays a diff, not an archaeology dig. (Zero `unsafe` today anyway.) +# # Vendored + trimmed copy of the `usbip` crate (jiegec/usbip v0.8.0, MIT), reduced to the # USB/IP *server simulation* path only: we present a virtual Steam Deck and let the local # `vhci_hcd` attach it. The upstream crate hard-depends on `rusb`→`libusb1-sys` (for its USB diff --git a/crates/punktfunk-tray/src/main.rs b/crates/punktfunk-tray/src/main.rs index 387e2a7e..6f6eef46 100644 --- a/crates/punktfunk-tray/src/main.rs +++ b/crates/punktfunk-tray/src/main.rs @@ -11,7 +11,6 @@ //! details. Windows-subsystem binary — a console exe in the HKLM Run key would flash a terminal //! window at every sign-in. // Unsafe-proof program: every `unsafe {}` in the tray carries a `// SAFETY:` proof. -#![deny(clippy::undocumented_unsafe_blocks)] #![cfg_attr(windows, windows_subsystem = "windows")] #[cfg(target_os = "linux")] diff --git a/crates/pyrowave-sys/src/lib.rs b/crates/pyrowave-sys/src/lib.rs index 9b91a0e8..bf79abfc 100644 --- a/crates/pyrowave-sys/src/lib.rs +++ b/crates/pyrowave-sys/src/lib.rs @@ -9,6 +9,11 @@ #![allow(non_snake_case)] // Bindgen output for a C API: u128 layout warnings and the like are upstream's concern. #![allow(improper_ctypes)] +// The workspace-wide undocumented_unsafe_blocks deny cannot apply to GENERATED code: bindgen +// emits `unsafe {}` in layout tests/accessors and nobody hand-writes proofs into OUT_DIR. This +// crate is bindings-only by charter (the safe wrapper lives with the consumer), so the allow is +// crate-wide; the hand-written link-sanity test below still carries its proof by convention. +#![allow(clippy::undocumented_unsafe_blocks)] #[cfg(any(target_os = "linux", target_os = "windows"))] include!(concat!(env!("OUT_DIR"), "/bindings.rs")); @@ -23,6 +28,8 @@ mod tests { #[test] fn api_version_matches_vendored_pin() { let (mut major, mut minor, mut patch) = (0u32, 0u32, 0u32); + // SAFETY: the version query writes three u32s through live local out-pointers and + // touches no device or global state. unsafe { pyrowave_get_api_version(&mut major, &mut minor, &mut patch) }; assert_eq!((major, minor, patch), (0, 4, 0), "vendored pyrowave API version moved — re-check the §4.2 protocol coupling before bumping"); } diff --git a/packaging/windows/drivers/Cargo.toml b/packaging/windows/drivers/Cargo.toml index 76ff5e8b..837613b0 100644 --- a/packaging/windows/drivers/Cargo.toml +++ b/packaging/windows/drivers/Cargo.toml @@ -15,6 +15,17 @@ version = "0.0.1" license = "MIT OR Apache-2.0" publish = false +# The same unsafe discipline as the main workspace (see its Cargo.toml for the full rationale). +# Restated here because THIS IS A SEPARATE WORKSPACE: the main tree's [workspace.lints] does not +# reach these crates, so any "workspace-wide" lint claim has to be made here too or it is false +# for the driver surface. Every member opts in with `[lints] workspace = true`. +# (`unsafe_op_in_unsafe_fn` is already the edition-2024 default; deny pins it explicitly.) +[workspace.lints.rust] +unsafe_op_in_unsafe_fn = "deny" + +[workspace.lints.clippy] +undocumented_unsafe_blocks = "deny" + [workspace.dependencies] wdk = "0.4.1" wdk-sys = "0.5.1" diff --git a/packaging/windows/drivers/pf-gamepad/Cargo.toml b/packaging/windows/drivers/pf-gamepad/Cargo.toml index a39bc8c3..24343c3b 100644 --- a/packaging/windows/drivers/pf-gamepad/Cargo.toml +++ b/packaging/windows/drivers/pf-gamepad/Cargo.toml @@ -32,3 +32,6 @@ pf-umdf-util.workspace = true default = ["hid"] hid = ["wdk-sys/hid"] nightly = ["wdk-sys/nightly", "wdk/nightly"] + +[lints] +workspace = true diff --git a/packaging/windows/drivers/pf-gamepad/src/lib.rs b/packaging/windows/drivers/pf-gamepad/src/lib.rs index cd60fb3a..958ba612 100644 --- a/packaging/windows/drivers/pf-gamepad/src/lib.rs +++ b/packaging/windows/drivers/pf-gamepad/src/lib.rs @@ -16,8 +16,6 @@ #![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)] // Every remaining `unsafe {}` (all WDF setup FFI) must carry a `// SAFETY:` proof. -#![deny(unsafe_op_in_unsafe_fn)] -#![deny(clippy::undocumented_unsafe_blocks)] use core::sync::atomic::{AtomicPtr, AtomicU32, Ordering}; diff --git a/packaging/windows/drivers/pf-mouse/Cargo.toml b/packaging/windows/drivers/pf-mouse/Cargo.toml index 64fb4400..9dfa2143 100644 --- a/packaging/windows/drivers/pf-mouse/Cargo.toml +++ b/packaging/windows/drivers/pf-mouse/Cargo.toml @@ -30,3 +30,6 @@ pf-umdf-util.workspace = true default = ["hid"] hid = ["wdk-sys/hid"] nightly = ["wdk-sys/nightly", "wdk/nightly"] + +[lints] +workspace = true diff --git a/packaging/windows/drivers/pf-mouse/src/lib.rs b/packaging/windows/drivers/pf-mouse/src/lib.rs index a7b4ffbd..da6fce01 100644 --- a/packaging/windows/drivers/pf-mouse/src/lib.rs +++ b/packaging/windows/drivers/pf-mouse/src/lib.rs @@ -23,8 +23,6 @@ #![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)] // Every remaining `unsafe {}` (all WDF setup FFI) must carry a `// SAFETY:` proof. -#![deny(unsafe_op_in_unsafe_fn)] -#![deny(clippy::undocumented_unsafe_blocks)] use core::sync::atomic::{AtomicPtr, AtomicU32, Ordering}; diff --git a/packaging/windows/drivers/pf-umdf-util/Cargo.toml b/packaging/windows/drivers/pf-umdf-util/Cargo.toml index a5a03134..6855abc4 100644 --- a/packaging/windows/drivers/pf-umdf-util/Cargo.toml +++ b/packaging/windows/drivers/pf-umdf-util/Cargo.toml @@ -15,3 +15,6 @@ description = "punktfunk UMDF driver util: safe shared-memory + sealed-channel + [dependencies] wdk-sys.workspace = true pf-driver-proto.workspace = true + +[lints] +workspace = true diff --git a/packaging/windows/drivers/pf-umdf-util/src/lib.rs b/packaging/windows/drivers/pf-umdf-util/src/lib.rs index c5992bd2..9abf12f0 100644 --- a/packaging/windows/drivers/pf-umdf-util/src/lib.rs +++ b/packaging/windows/drivers/pf-umdf-util/src/lib.rs @@ -19,12 +19,9 @@ //! `pf_gamepad`/`pf_mouse` tell the host, over the device stack, which process is serving this //! devnode. That is what the host trusts instead of the LocalService-writable bootstrap mailbox. //! -//! Lint gates (mirrored in every driver crate, enforced by the drivers CI clippy step): -//! `unsafe_op_in_unsafe_fn` + `clippy::undocumented_unsafe_blocks` — every remaining `unsafe {}` -//! must carry a `// SAFETY:` proof. - -#![deny(unsafe_op_in_unsafe_fn)] -#![deny(clippy::undocumented_unsafe_blocks)] +//! Lint gates (workspace-wide via this workspace's `[workspace.lints]`, enforced by the drivers +//! CI clippy step): `unsafe_op_in_unsafe_fn` + `clippy::undocumented_unsafe_blocks` — every +//! remaining `unsafe {}` must carry a `// SAFETY:` proof. pub mod channel; pub mod hid; diff --git a/packaging/windows/drivers/pf-umdf-util/src/wdf.rs b/packaging/windows/drivers/pf-umdf-util/src/wdf.rs index d72f11c8..033869b6 100644 --- a/packaging/windows/drivers/pf-umdf-util/src/wdf.rs +++ b/packaging/windows/drivers/pf-umdf-util/src/wdf.rs @@ -29,6 +29,8 @@ pub struct Request(WDFREQUEST); impl Request { /// Wrap the raw request handed to the current framework callback. /// + // unsafe-fn-no-op-ok: contract-deferring constructor — the body only wraps the handle; every + // later `complete`/`forward` call trusts the framework-liveness promised here. /// # Safety /// `raw` must be the live, framework-provided `WDFREQUEST` of the callback invocation this is /// called from (WDF owns handle validity; a forged/dangling handle is framework UB). diff --git a/packaging/windows/drivers/pf-vdisplay/Cargo.toml b/packaging/windows/drivers/pf-vdisplay/Cargo.toml index 884d44bb..e408a0af 100644 --- a/packaging/windows/drivers/pf-vdisplay/Cargo.toml +++ b/packaging/windows/drivers/pf-vdisplay/Cargo.toml @@ -42,3 +42,6 @@ features = [ "Win32_Graphics_Dxgi", "Win32_Graphics_Dxgi_Common", ] + +[lints] +workspace = true diff --git a/packaging/windows/drivers/pf-vdisplay/src/lib.rs b/packaging/windows/drivers/pf-vdisplay/src/lib.rs index b92fead1..4665282b 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/lib.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/lib.rs @@ -14,8 +14,6 @@ // proof. An IddCx display driver is inherently FFI-bound (D3D11 / IddCx DDIs / cross-process shared // textures), so it can't be unsafe-FREE the way the gamepad drivers now are (their logic moved onto the // safe `pf_umdf_util` layer); these gates make it unsafe-AUDITED instead, and stop it regressing. -#![deny(unsafe_op_in_unsafe_fn)] -#![deny(clippy::undocumented_unsafe_blocks)] #[macro_use] mod log; diff --git a/packaging/windows/drivers/pf-xusb/Cargo.toml b/packaging/windows/drivers/pf-xusb/Cargo.toml index 8cf59986..924a5e1a 100644 --- a/packaging/windows/drivers/pf-xusb/Cargo.toml +++ b/packaging/windows/drivers/pf-xusb/Cargo.toml @@ -29,3 +29,6 @@ pf-umdf-util.workspace = true [features] default = [] nightly = ["wdk-sys/nightly", "wdk/nightly"] + +[lints] +workspace = true diff --git a/packaging/windows/drivers/pf-xusb/src/lib.rs b/packaging/windows/drivers/pf-xusb/src/lib.rs index 4a8ce4c0..6711c810 100644 --- a/packaging/windows/drivers/pf-xusb/src/lib.rs +++ b/packaging/windows/drivers/pf-xusb/src/lib.rs @@ -22,8 +22,6 @@ #![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)] // Every remaining `unsafe {}` (all WDF setup FFI) must carry a `// SAFETY:` proof. -#![deny(unsafe_op_in_unsafe_fn)] -#![deny(clippy::undocumented_unsafe_blocks)] use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering}; use pf_driver_proto::gamepad::XusbShm; diff --git a/packaging/windows/drivers/wdk-iddcx/Cargo.toml b/packaging/windows/drivers/wdk-iddcx/Cargo.toml index f22a9457..9bee212b 100644 --- a/packaging/windows/drivers/wdk-iddcx/Cargo.toml +++ b/packaging/windows/drivers/wdk-iddcx/Cargo.toml @@ -12,3 +12,6 @@ publish = false [dependencies] wdk-sys = { workspace = true, features = ["iddcx"] } + +[lints] +workspace = true diff --git a/packaging/windows/drivers/wdk-iddcx/src/lib.rs b/packaging/windows/drivers/wdk-iddcx/src/lib.rs index 45c8573f..428101af 100644 --- a/packaging/windows/drivers/wdk-iddcx/src/lib.rs +++ b/packaging/windows/drivers/wdk-iddcx/src/lib.rs @@ -12,8 +12,6 @@ #![allow(non_snake_case, clippy::missing_safety_doc)] // P0 lint (audit §8): require explicit `unsafe {}` blocks inside `unsafe fn`s + a `// SAFETY:` proof on // each (this crate is the IddCx DDI dispatch layer — inherently unsafe, so audited, not unsafe-free). -#![deny(unsafe_op_in_unsafe_fn)] -#![deny(clippy::undocumented_unsafe_blocks)] pub use wdk_sys::iddcx; diff --git a/packaging/windows/drivers/wdk-probe/Cargo.toml b/packaging/windows/drivers/wdk-probe/Cargo.toml index 9036ef27..d2b768e6 100644 --- a/packaging/windows/drivers/wdk-probe/Cargo.toml +++ b/packaging/windows/drivers/wdk-probe/Cargo.toml @@ -27,3 +27,6 @@ wdk.workspace = true # do its WDF/DXGI types resolve to wdk-sys's (so the generated module compiles)? wdk-sys = { workspace = true, features = ["iddcx"] } pf-driver-proto.workspace = true + +[lints] +workspace = true diff --git a/packaging/windows/drivers/wdk-probe/src/iddcx_rt.rs b/packaging/windows/drivers/wdk-probe/src/iddcx_rt.rs index 289a7074..fc46f71a 100644 --- a/packaging/windows/drivers/wdk-probe/src/iddcx_rt.rs +++ b/packaging/windows/drivers/wdk-probe/src/iddcx_rt.rs @@ -26,6 +26,10 @@ unsafe fn ddi(index: i32) -> T { let table = (&raw const IddFunctions).cast::(); // SAFETY: `index` is a valid IddCx table slot; the slot holds a `PFN_*` whose layout is `T`. let slot = unsafe { table.add(index as usize) }; + // SAFETY: `slot` is in bounds of the stub-populated table (per this function's contract), + // pointer-aligned (a table of `PFN_IDD_CX` entries), and initialized by `IddCxStub` at driver + // load; every `PFN_*` is an `Option`, for which both null (None) and a populated + // entry are valid values to read as `T`. unsafe { slot.cast::().read() } } @@ -45,8 +49,12 @@ pub unsafe fn IddCxDeviceInitConfig( device_init: PWDFDEVICE_INIT, config: *const IDD_CX_CLIENT_CONFIG, ) -> NTSTATUS { + // SAFETY: index and PFN type name the same DDI (IddCxDeviceInitConfig), and this function's + // contract — call only during EvtDriverDeviceAdd — implies the driver is loaded and the + // stub's table populated. let f: PFN_IDDCXDEVICEINITCONFIG = unsafe { ddi(_IDDFUNCENUM::IddCxDeviceInitConfigTableIndex) }; + // SAFETY: the driver is loaded (same premise as above), so the stub globals are set. let g = unsafe { globals() }; // SAFETY: dispatching a populated DDI with the stub globals and caller-valid args. unsafe { (f.unwrap())(g, device_init, config) } @@ -55,8 +63,12 @@ pub unsafe fn IddCxDeviceInitConfig( /// # Safety /// `device` must be a valid `WDFDEVICE` previously configured via [`IddCxDeviceInitConfig`]. pub unsafe fn IddCxDeviceInitialize(device: WDFDEVICE) -> NTSTATUS { + // SAFETY: index and PFN type name the same DDI (IddCxDeviceInitialize), and this function's + // contract — `device` came out of a WdfDeviceCreate on an IddCx-configured init — implies + // the driver is loaded and the stub's table populated. let f: PFN_IDDCXDEVICEINITIALIZE = unsafe { ddi(_IDDFUNCENUM::IddCxDeviceInitializeTableIndex) }; + // SAFETY: the driver is loaded (same premise as above), so the stub globals are set. let g = unsafe { globals() }; // SAFETY: dispatching a populated DDI with the stub globals and a caller-valid device. unsafe { (f.unwrap())(g, device) } @@ -68,8 +80,12 @@ pub unsafe fn IddCxAdapterInitAsync( in_args: *const IDARG_IN_ADAPTER_INIT, out_args: *mut IDARG_OUT_ADAPTER_INIT, ) -> NTSTATUS { + // SAFETY: index and PFN type name the same DDI (IddCxAdapterInitAsync), and IddCx DDIs are + // only reachable from framework callbacks — i.e. with the driver loaded and the stub's + // table populated. let f: PFN_IDDCXADAPTERINITASYNC = unsafe { ddi(_IDDFUNCENUM::IddCxAdapterInitAsyncTableIndex) }; + // SAFETY: the driver is loaded (same premise as above), so the stub globals are set. let g = unsafe { globals() }; // SAFETY: dispatching a populated DDI with the stub globals and caller-valid args. unsafe { (f.unwrap())(g, in_args, out_args) } diff --git a/packaging/windows/drivers/wdk-probe/src/lib.rs b/packaging/windows/drivers/wdk-probe/src/lib.rs index 7a315e05..d3213ba8 100644 --- a/packaging/windows/drivers/wdk-probe/src/lib.rs +++ b/packaging/windows/drivers/wdk-probe/src/lib.rs @@ -4,7 +4,7 @@ //! stub link AND prove the `iddcx` subset is callable + links against `IddCxStub`. Also force-links the //! shared `pf-driver-proto` ABI crate (no_std + bytemuck) across the workspace boundary. -#![allow(non_snake_case, clippy::missing_safety_doc)] +#![allow(non_snake_case)] mod iddcx_rt; mod iddcx_surface_assert; @@ -29,6 +29,9 @@ static PROTO_GUID_LO: u64 = pf_driver_proto::PF_VDISPLAY_INTERFACE_GUID_U128 as #[unsafe(no_mangle)] pub static IddMinimumVersionRequired: ULONG = 4; +/// # Safety +/// Called by the UMDF host at driver load: `driver` and `registry_path` must be the +/// loader-provided driver object and registry path, both valid for the duration of the call. #[unsafe(export_name = "DriverEntry")] pub unsafe extern "system" fn driver_entry( driver: PDRIVER_OBJECT, @@ -57,6 +60,9 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI // IddCxStub (CI), and that a self-signed IddCx driver loads + dispatches (box); the full callback // surface + a valid adapter come with the driver port. At runtime IddCxDeviceInitConfig will reject // the null callbacks, but the call site is what links IddCxStub and exercises table dispatch. + // SAFETY: IDD_CX_CLIENT_CONFIG is a C aggregate of integers, pointers, and nullable + // callbacks (`Option`) — all-zero is a valid value (null callbacks, see above); + // Size is stamped on the next line. let mut cfg: IDD_CX_CLIENT_CONFIG = unsafe { core::mem::zeroed() }; cfg.Size = core::mem::size_of::() as ULONG; // SAFETY: device_init is the framework-provided init; cfg is a valid (if minimal) config. @@ -76,8 +82,11 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI // SAFETY: device is the just-created WDFDEVICE. let _ = unsafe { iddcx_rt::IddCxDeviceInitialize(device) }; - // SAFETY: zeroed adapter-init args — a link/dispatch reference, not a valid adapter (see above). + // SAFETY: IDARG_IN_ADAPTER_INIT is a C aggregate of handles/pointers/integers — all-zero is + // a valid value (a link/dispatch reference, not a valid adapter; see above). let in_args: IDARG_IN_ADAPTER_INIT = unsafe { core::mem::zeroed() }; + // SAFETY: IDARG_OUT_ADAPTER_INIT is a C aggregate of handles — all-zero is a valid value, + // and the call below only writes through it. let mut out_args: IDARG_OUT_ADAPTER_INIT = unsafe { core::mem::zeroed() }; // SAFETY: in/out args are valid local storage for the call. let _ = unsafe { iddcx_rt::IddCxAdapterInitAsync(&in_args, &mut out_args) }; diff --git a/packaging/windows/pf-vkhdr-layer/Cargo.toml b/packaging/windows/pf-vkhdr-layer/Cargo.toml index e6876a4e..bbd08740 100644 --- a/packaging/windows/pf-vkhdr-layer/Cargo.toml +++ b/packaging/windows/pf-vkhdr-layer/Cargo.toml @@ -13,6 +13,14 @@ crate-type = ["cdylib"] [dependencies] ash = "=0.38.0+1.3.281" +# This package is its own workspace (see below), so the main workspace's lint tables never reach +# it — the unsafe discipline has to be restated here or it silently doesn't apply. +[lints.rust] +unsafe_op_in_unsafe_fn = "deny" + +[lints.clippy] +undocumented_unsafe_blocks = "deny" + [profile.release] opt-level = 2 panic = "abort" diff --git a/packaging/windows/pf-vkhdr-layer/src/lib.rs b/packaging/windows/pf-vkhdr-layer/src/lib.rs index 92ae1f34..6be9f881 100644 --- a/packaging/windows/pf-vkhdr-layer/src/lib.rs +++ b/packaging/windows/pf-vkhdr-layer/src/lib.rs @@ -23,9 +23,17 @@ //! Off-switches: the loader-standard `DISABLE_PF_VKHDR=1` (disables the whole layer), and //! `PF_VKHDR_EXCLUDE` (comma/semicolon list of exe basenames to skip — defaults include known //! kernel-anti-cheat titles). `PF_VKHDR_LOG=1` enables a debug log in `%TEMP%\pf_vkhdr_layer.log`. +//! +//! ## Safety model +//! This cdylib runs inside someone else's process, called by the Vulkan loader. Two contract +//! sources cover every unsafe operation here: the **loader layer protocol** (negotiate struct, +//! per-layer chain links, dispatchable handles whose first word is the loader's dispatch-table +//! pointer) and **Vulkan valid-usage rules** the application must already uphold for the ICD +//! (NUL-terminated command names, valid create-info chains, count/array pairs in the two-call +//! idiom). Every `unsafe` block cites the specific clause it leans on. The layer targets x86_64 +//! only (the two hand-computed struct offsets below say so explicitly). #![allow(non_snake_case)] -#![allow(clippy::missing_safety_doc)] #![allow(clippy::too_many_arguments)] // HWND / HMONITOR etc. deliberately mirror the Win32 names. #![allow(clippy::upper_case_acronyms)] @@ -97,6 +105,14 @@ struct SurfaceFormat2Raw { p_next: *mut c_void, surface_format: vk::SurfaceFormatKHR, } +// Layout proof for the mirror: arrays of it are handed to the ICD and stamped over the caller's +// VkSurfaceFormat2KHR array, so a size or alignment drift would corrupt either side. +const _: () = assert!( + std::mem::size_of::() == std::mem::size_of::() +); +const _: () = assert!( + std::mem::align_of::() == std::mem::align_of::() +); // ---- ICD function-pointer typedefs we call down to (raw pointers, no lifetimes) ---- type FnCreateInstance = @@ -128,6 +144,9 @@ type FnCreateWin32Surface = unsafe extern "system" fn( ) -> vk::Result; type FnDestroySurface = unsafe extern "system" fn(vk::Instance, vk::SurfaceKHR, *const c_void); +// Both maps' values are raw `extern "system"` fn pointers plus plain handles; fn pointers are +// process-global code addresses and implement Send/Sync intrinsically, so the auto traits hold +// and no `unsafe impl Send` is needed (two used to sit here as unproven markers). struct InstanceData { instance: vk::Instance, next_gipa: PfnGipa, @@ -138,12 +157,10 @@ struct InstanceData { create_win32_surface: Option, destroy_surface: Option, } -unsafe impl Send for InstanceData {} struct DeviceData { next_gdpa: PfnGdpa, } -unsafe impl Send for DeviceData {} fn instances() -> &'static Mutex> { static M: OnceLock>> = OnceLock::new(); @@ -159,21 +176,47 @@ fn surface_hwnds() -> &'static Mutex> { M.get_or_init(|| Mutex::new(HashMap::new())) } -// dispatch key = first pointer word of a dispatchable handle (loader dispatch table ptr). +/// Dispatch key of a dispatchable handle. +/// +/// # Safety +/// `raw` must be the value of a **live dispatchable** Vulkan handle (`VkInstance`, +/// `VkPhysicalDevice`, `VkDevice`). The loader ABI mandates that every dispatchable object's +/// first pointer-sized word is the loader's dispatch-table pointer, which is what makes the read +/// in-bounds and initialized — and what makes it a stable per-chain key. #[inline] unsafe fn key(raw: u64) -> usize { - *(raw as usize as *const usize) + // SAFETY: per this function's contract, `raw` points at a live dispatchable object whose + // first pointer-sized word exists and is initialized (the loader wrote it at creation). + unsafe { *(raw as usize as *const usize) } } + +/// Reinterpret a function address as the loader's type-erased void-function pointer. +/// +/// # Safety +/// `p` must be the address of an `extern "system"` function. Whoever receives the returned PFN +/// must cast it back to the exact prototype of the command it was queried under before calling — +/// which is precisely what the Vulkan `*ProcAddr` contract obliges callers to do. #[inline] unsafe fn as_pfn(p: *const c_void) -> vk::PFN_vkVoidFunction { - Some(std::mem::transmute::< - *const c_void, - unsafe extern "system" fn(), - >(p)) + // SAFETY: data and function pointers have identical size and representation on all Windows + // targets, and per this function's contract `p` is a real `extern "system"` fn address. + Some(unsafe { std::mem::transmute::<*const c_void, unsafe extern "system" fn()>(p) }) } + +/// Resolve `name` down-chain and reinterpret the result as fn-pointer type `T`. +/// +/// # Safety +/// `gipa` must be a valid `vkGetInstanceProcAddr` for `inst` (or for the null instance when +/// resolving global commands), and `T` must be the exact fn-pointer prototype of the Vulkan +/// command named by `name` — `transmute_copy` erases all type checking between them. #[inline] unsafe fn resolve(gipa: PfnGipa, inst: vk::Instance, name: &CStr) -> Option { - gipa(inst, name.as_ptr()).map(|f| std::mem::transmute_copy::<_, T>(&f)) + // SAFETY: per this function's contract `gipa` is a valid loader/ICD GetInstanceProcAddr for + // `inst`, and `name` is a live NUL-terminated string for the duration of the call. + let f = unsafe { gipa(inst, name.as_ptr()) }; + // SAFETY: `f` was returned for `name`, and per this function's contract `T` is that + // command's exact prototype; both sides are fn pointers of identical size. + f.map(|f| unsafe { std::mem::transmute_copy::<_, T>(&f) }) } fn log(msg: &str) { @@ -356,21 +399,34 @@ mod hdr { const GET_ADVANCED_COLOR_INFO: i32 = 9; const MONITOR_DEFAULTTONEAREST: u32 = 2; - unsafe fn active_paths() -> Vec { + // Safe fn: every invariant below is local (out-params point at live locals / exact-length + // Vecs); callers carry no obligations. + fn active_paths() -> Vec { let (mut np, mut nm) = (0u32, 0u32); - if GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut np, &mut nm) != 0 || np == 0 { + // SAFETY: both out-pointers come from live local `u32`s that outlive the call. + if unsafe { GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut np, &mut nm) } != 0 + || np == 0 + { return Vec::new(); } - let mut pa: Vec = vec![std::mem::zeroed(); np as usize]; - let mut ma: Vec = vec![std::mem::zeroed(); nm as usize]; - if QueryDisplayConfig( - QDC_ONLY_ACTIVE_PATHS, - &mut np, - pa.as_mut_ptr(), - &mut nm, - ma.as_mut_ptr(), - std::ptr::null_mut(), - ) != 0 + // SAFETY: PathInfo is a #[repr(C)] aggregate of integers — all-zero is a valid value. + let mut pa: Vec = vec![unsafe { std::mem::zeroed() }; np as usize]; + // SAFETY: ModeInfo is an opaque byte blob — all-zero is a valid value. + let mut ma: Vec = vec![unsafe { std::mem::zeroed() }; nm as usize]; + // SAFETY: `pa`/`ma` hold exactly `np`/`nm` elements and those counts are passed by + // pointer as the in/out capacities: QueryDisplayConfig writes at most that many entries + // (if the topology grew between the two calls it returns ERROR_INSUFFICIENT_BUFFER + // rather than writing past the end) and shrinks np/nm to what it actually wrote. + if unsafe { + QueryDisplayConfig( + QDC_ONLY_ACTIVE_PATHS, + &mut np, + pa.as_mut_ptr(), + &mut nm, + ma.as_mut_ptr(), + std::ptr::null_mut(), + ) + } != 0 { return Vec::new(); } @@ -378,38 +434,52 @@ mod hdr { pa } - unsafe fn target_hdr_enabled(p: &PathInfo) -> bool { - let mut ai: AdvInfo = std::mem::zeroed(); + fn target_hdr_enabled(p: &PathInfo) -> bool { + // SAFETY: AdvInfo is a #[repr(C)] aggregate of integers — all-zero is a valid value. + let mut ai: AdvInfo = unsafe { std::mem::zeroed() }; ai.header.typ = GET_ADVANCED_COLOR_INFO; ai.header.size = std::mem::size_of::() as u32; ai.header.adapter = p.tgt.adapter; ai.header.id = p.tgt.id; - if DisplayConfigGetDeviceInfo(&mut ai as *mut _ as *mut c_void) != 0 { + // SAFETY: the request header carries this struct's exact size, which is the documented + // bound for DisplayConfigGetDeviceInfo's write; `ai` is a live local across the call. + if unsafe { DisplayConfigGetDeviceInfo(&mut ai as *mut _ as *mut c_void) } != 0 { return false; } // value bitfield: bit0 advancedColorSupported, bit1 advancedColorEnabled. (ai.value & 0b10) != 0 } - unsafe fn source_gdi(p: &PathInfo) -> [u16; 32] { - let mut sn: SourceName = std::mem::zeroed(); + fn source_gdi(p: &PathInfo) -> [u16; 32] { + // SAFETY: SourceName is a #[repr(C)] aggregate of integers — all-zero is a valid value. + let mut sn: SourceName = unsafe { std::mem::zeroed() }; sn.header.typ = GET_SOURCE_NAME; sn.header.size = std::mem::size_of::() as u32; sn.header.adapter = p.src.adapter; sn.header.id = p.src.id; - let _ = DisplayConfigGetDeviceInfo(&mut sn as *mut _ as *mut c_void); + // SAFETY: the request header carries this struct's exact size, which is the documented + // bound for DisplayConfigGetDeviceInfo's write; `sn` is a live local across the call. + let _ = unsafe { DisplayConfigGetDeviceInfo(&mut sn as *mut _ as *mut c_void) }; sn.gdi } /// Is HDR (Windows advanced color) currently enabled on the display this surface lives on? /// `hwnd == 0`/unknown falls back to "any active display has HDR enabled". - pub unsafe fn enabled_for(hwnd: HWND) -> bool { + /// + /// Safe fn: `MonitorFromWindow` with `DEFAULTTONEAREST` tolerates any HWND value — including + /// a destroyed or foreign one (our map can be stale) — so callers carry no obligations. + pub fn enabled_for(hwnd: HWND) -> bool { let paths = active_paths(); if hwnd != 0 { - let mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - let mut mi: MonitorInfoExW = std::mem::zeroed(); + // SAFETY: MonitorFromWindow accepts arbitrary HWND values with DEFAULTTONEAREST + // (falling back to the nearest/primary monitor) and takes nothing by pointer. + let mon = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + // SAFETY: MonitorInfoExW is a #[repr(C)] aggregate of integers — all-zero is valid. + let mut mi: MonitorInfoExW = unsafe { std::mem::zeroed() }; mi.cb = std::mem::size_of::() as u32; - if GetMonitorInfoW(mon, &mut mi) != 0 { + // SAFETY: `mi.cb` carries the struct's exact size, which is the documented bound for + // GetMonitorInfoW's write; `mi` is a live local across the call. + if unsafe { GetMonitorInfoW(mon, &mut mi) } != 0 { for p in &paths { if source_gdi(p) == mi.sz_device { return target_hdr_enabled(p); @@ -417,12 +487,13 @@ mod hdr { } } } - paths.iter().any(|p| target_hdr_enabled(p)) + paths.iter().any(target_hdr_enabled) } } -/// Should we inject HDR formats for this surface right now? -unsafe fn should_inject(surface: vk::SurfaceKHR) -> bool { +/// Should we inject HDR formats for this surface right now? Safe fn — the surface handle is only +/// used as a map key, and a stale/unknown HWND degrades to the any-display fallback. +fn should_inject(surface: vk::SurfaceKHR) -> bool { if !injection_allowed_for_process() { return false; } @@ -436,6 +507,12 @@ unsafe fn should_inject(surface: vk::SurfaceKHR) -> bool { // ---- entry point ---- +/// Layer negotiation entry point; the loader calls this export when it loads the DLL. +/// +/// # Safety +/// `p` must be null or point to a live `NegotiateLayerInterface` the caller has exclusive access +/// to for the duration of the call. The Vulkan loader — the only intended caller of this +/// export — guarantees exactly that for the negotiate handshake. #[no_mangle] pub unsafe extern "system" fn vkNegotiateLoaderLayerInterfaceVersion( p: *mut NegotiateLayerInterface, @@ -443,7 +520,9 @@ pub unsafe extern "system" fn vkNegotiateLoaderLayerInterfaceVersion( if p.is_null() { return vk::Result::ERROR_INITIALIZATION_FAILED; } - let s = &mut *p; + // SAFETY: `p` is non-null, and per this export's contract the loader hands us exclusive + // access to a live NegotiateLayerInterface for the duration of the call. + let s = unsafe { &mut *p }; if s.s_type != LAYER_NEGOTIATE_INTERFACE_STRUCT { return vk::Result::ERROR_INITIALIZATION_FAILED; } @@ -466,27 +545,46 @@ unsafe extern "system" fn layer_gipa( if p_name.is_null() { return None; } - match CStr::from_ptr(p_name).to_bytes() { - b"vkGetInstanceProcAddr" => as_pfn(layer_gipa as *const c_void), - b"vkGetDeviceProcAddr" => as_pfn(layer_gdpa as *const c_void), - b"vkCreateInstance" => as_pfn(create_instance as *const c_void), - b"vkDestroyInstance" => as_pfn(destroy_instance as *const c_void), - b"vkCreateDevice" => as_pfn(create_device as *const c_void), - b"vkGetPhysicalDeviceSurfaceFormatsKHR" => as_pfn(get_surface_formats as *const c_void), - b"vkGetPhysicalDeviceSurfaceFormats2KHR" => as_pfn(get_surface_formats2 as *const c_void), - b"vkCreateWin32SurfaceKHR" => as_pfn(create_win32_surface as *const c_void), - b"vkDestroySurfaceKHR" => as_pfn(destroy_surface as *const c_void), - _ => { - if instance == vk::Instance::null() { - return None; + // SAFETY: `p_name` is non-null, and vkGetInstanceProcAddr's valid-usage rules require pName + // to be a valid NUL-terminated string for the duration of the call. + let name = unsafe { CStr::from_ptr(p_name) }.to_bytes(); + // SAFETY: every arm hands `as_pfn` the address of the `extern "system"` hook this layer + // substitutes for exactly that command name; the *ProcAddr contract obliges the caller to + // cast the returned PFN back to the named command's prototype before invoking it, which is + // what makes the type-erased transmute inside `as_pfn` sound for each arm. + let hook = unsafe { + match name { + b"vkGetInstanceProcAddr" => as_pfn(layer_gipa as *const c_void), + b"vkGetDeviceProcAddr" => as_pfn(layer_gdpa as *const c_void), + b"vkCreateInstance" => as_pfn(create_instance as *const c_void), + b"vkDestroyInstance" => as_pfn(destroy_instance as *const c_void), + b"vkCreateDevice" => as_pfn(create_device as *const c_void), + b"vkGetPhysicalDeviceSurfaceFormatsKHR" => as_pfn(get_surface_formats as *const c_void), + b"vkGetPhysicalDeviceSurfaceFormats2KHR" => { + as_pfn(get_surface_formats2 as *const c_void) } - let next = { - let g = instances().lock().ok()?; - g.get(&key(instance.as_raw())).map(|d| d.next_gipa) - }; - next.and_then(|gipa| gipa(instance, p_name)) + b"vkCreateWin32SurfaceKHR" => as_pfn(create_win32_surface as *const c_void), + b"vkDestroySurfaceKHR" => as_pfn(destroy_surface as *const c_void), + _ => None, } + }; + if hook.is_some() { + return hook; } + if instance == vk::Instance::null() { + return None; + } + let next = { + let g = instances().lock().ok()?; + // SAFETY: `instance` is non-null, and vkGetInstanceProcAddr's valid-usage rules make a + // non-null instance argument a live instance handle — a dispatchable object whose first + // word is the dispatch key. + g.get(&unsafe { key(instance.as_raw()) }) + .map(|d| d.next_gipa) + }; + // SAFETY: `next` is the down-chain GetInstanceProcAddr captured from the loader's link at + // create_instance for this very chain; `p_name` is still valid NUL-terminated. + next.and_then(|gipa| unsafe { gipa(instance, p_name) }) } unsafe extern "system" fn layer_gpdpa( @@ -496,20 +594,37 @@ unsafe extern "system" fn layer_gpdpa( if p_name.is_null() { return None; } - match CStr::from_ptr(p_name).to_bytes() { - b"vkGetPhysicalDeviceSurfaceFormatsKHR" => as_pfn(get_surface_formats as *const c_void), - b"vkGetPhysicalDeviceSurfaceFormats2KHR" => as_pfn(get_surface_formats2 as *const c_void), - _ => { - if instance == vk::Instance::null() { - return None; + // SAFETY: `p_name` is non-null, and the GetPhysicalDeviceProcAddr contract mirrors + // vkGetInstanceProcAddr's: pName is a valid NUL-terminated string for the call. + let name = unsafe { CStr::from_ptr(p_name) }.to_bytes(); + // SAFETY: both arms hand `as_pfn` the address of the `extern "system"` hook this layer + // substitutes for exactly that command name; the *ProcAddr contract obliges the caller to + // cast the returned PFN back to that command's prototype before invoking it. + let hook = unsafe { + match name { + b"vkGetPhysicalDeviceSurfaceFormatsKHR" => as_pfn(get_surface_formats as *const c_void), + b"vkGetPhysicalDeviceSurfaceFormats2KHR" => { + as_pfn(get_surface_formats2 as *const c_void) } - let next = { - let g = instances().lock().ok()?; - g.get(&key(instance.as_raw())).and_then(|d| d.next_gpdpa) - }; - next.and_then(|gpdpa| gpdpa(instance, p_name)) + _ => None, } + }; + if hook.is_some() { + return hook; } + if instance == vk::Instance::null() { + return None; + } + let next = { + let g = instances().lock().ok()?; + // SAFETY: `instance` is non-null and (per the caller's contract) a live instance + // handle — a dispatchable object whose first word is the dispatch key. + g.get(&unsafe { key(instance.as_raw()) }) + .and_then(|d| d.next_gpdpa) + }; + // SAFETY: `next` is the down-chain GPDPA captured from the loader's link at create_instance + // for this very chain; `p_name` is still valid NUL-terminated. + next.and_then(|gpdpa| unsafe { gpdpa(instance, p_name) }) } unsafe extern "system" fn layer_gdpa( @@ -519,8 +634,13 @@ unsafe extern "system" fn layer_gdpa( if p_name.is_null() { return None; } - if CStr::from_ptr(p_name).to_bytes() == b"vkGetDeviceProcAddr" { - return as_pfn(layer_gdpa as *const c_void); + // SAFETY: `p_name` is non-null, and vkGetDeviceProcAddr's valid-usage rules require pName to + // be a valid NUL-terminated string for the duration of the call. + let name = unsafe { CStr::from_ptr(p_name) }.to_bytes(); + if name == b"vkGetDeviceProcAddr" { + // SAFETY: `layer_gdpa` is an `extern "system"` fn whose prototype is exactly what the + // caller will cast the returned PFN back to for this name (*ProcAddr contract). + return unsafe { as_pfn(layer_gdpa as *const c_void) }; } if device == vk::Device::null() { return None; @@ -530,23 +650,38 @@ unsafe extern "system" fn layer_gdpa( Ok(g) => g, Err(_) => return None, }; - g.get(&key(device.as_raw())).map(|d| d.next_gdpa) + // SAFETY: `device` is non-null, and vkGetDeviceProcAddr's valid-usage rules make it a + // live device handle — a dispatchable object whose first word is the dispatch key. + g.get(&unsafe { key(device.as_raw()) }).map(|d| d.next_gdpa) }; - next.and_then(|gdpa| gdpa(device, p_name)) + // SAFETY: `next` is the down-chain GetDeviceProcAddr captured from the loader's link at + // create_device for this very chain; `p_name` is still valid NUL-terminated. + next.and_then(|gdpa| unsafe { gdpa(device, p_name) }) } // ---- instance chain ---- +/// Walk `p_ci`'s pNext chain for the loader's layer-link node. +/// +/// # Safety +/// `p_ci` must point to a valid create-info struct whose `pNext` chain is a well-formed list in +/// which every node begins with the `{sType, pNext}` header — the loader builds exactly that +/// chain for the create call that invokes us. unsafe fn find_instance_link(p_ci: *const c_void) -> *mut LayerInstanceCreateInfo { - let mut node = (*(p_ci as *const BaseIn)).p_next as *const BaseIn; - while !node.is_null() { - if (*node).s_type.as_raw() == LOADER_INSTANCE_CREATE_INFO { - let lci = node as *mut LayerInstanceCreateInfo; - if (*lci).function == VK_LAYER_LINK_INFO { - return lci; + // SAFETY: per this function's contract, `p_ci` and every non-null `pNext` reached from it + // point at nodes beginning with a {sType, pNext} header; a node whose sType says it is the + // loader's instance create-info really is a LayerInstanceCreateInfo (the loader wrote it). + unsafe { + let mut node = (*(p_ci as *const BaseIn)).p_next as *const BaseIn; + while !node.is_null() { + if (*node).s_type.as_raw() == LOADER_INSTANCE_CREATE_INFO { + let lci = node as *mut LayerInstanceCreateInfo; + if (*lci).function == VK_LAYER_LINK_INFO { + return lci; + } } + node = (*node).p_next as *const BaseIn; } - node = (*node).p_next as *const BaseIn; } ptr::null_mut() } @@ -556,40 +691,67 @@ unsafe extern "system" fn create_instance( p_alloc: *const c_void, p_inst: *mut vk::Instance, ) -> vk::Result { - let lci = find_instance_link(p_ci); + // SAFETY: the loader invokes this hook with a valid VkInstanceCreateInfo whose pNext chain + // it built — the chain find_instance_link's contract requires. + let lci = unsafe { find_instance_link(p_ci) }; if lci.is_null() { return vk::Result::ERROR_INITIALIZATION_FAILED; } - let link = (*lci).u; - if link.is_null() { - return vk::Result::ERROR_INITIALIZATION_FAILED; - } - let next_gipa = (*link).next_gipa; - let next_gpdpa = (*link).next_gpdpa; - (*lci).u = (*link).p_next; + // SAFETY: `lci` is non-null and points into the loader's live chain. Its `u` link (checked + // non-null before use) is this layer's link entry; reading the down-chain pointers out of it + // and advancing `(*lci).u` to the next link is the layer protocol every layer must follow + // before calling down. + let (next_gipa, next_gpdpa) = unsafe { + let link = (*lci).u; + if link.is_null() { + return vk::Result::ERROR_INITIALIZATION_FAILED; + } + let gipa = (*link).next_gipa; + let gpdpa = (*link).next_gpdpa; + (*lci).u = (*link).p_next; + (gipa, gpdpa) + }; + // SAFETY: `next_gipa` is the loader-supplied down-chain GIPA; vkCreateInstance is a global + // command resolvable from the null instance, and FnCreateInstance mirrors its prototype. let create: FnCreateInstance = - match resolve(next_gipa, vk::Instance::null(), c"vkCreateInstance") { + match unsafe { resolve(next_gipa, vk::Instance::null(), c"vkCreateInstance") } { Some(f) => f, None => return vk::Result::ERROR_INITIALIZATION_FAILED, }; - let res = create(p_ci, p_alloc, p_inst); + // SAFETY: forwarding the loader's own arguments unchanged to the down-chain create, which + // expects exactly this (p_ci with the link advanced, the caller's allocator, the caller's + // out-pointer). + let res = unsafe { create(p_ci, p_alloc, p_inst) }; if res != vk::Result::SUCCESS { return res; } - let inst = *p_inst; - let data = InstanceData { - instance: inst, - next_gipa, - next_gpdpa, - destroy_instance: resolve(next_gipa, inst, c"vkDestroyInstance"), - get_surface_formats: resolve(next_gipa, inst, c"vkGetPhysicalDeviceSurfaceFormatsKHR"), - get_surface_formats2: resolve(next_gipa, inst, c"vkGetPhysicalDeviceSurfaceFormats2KHR"), - create_win32_surface: resolve(next_gipa, inst, c"vkCreateWin32SurfaceKHR"), - destroy_surface: resolve(next_gipa, inst, c"vkDestroySurfaceKHR"), + // SAFETY: vkCreateInstance requires pInstance to be a valid pointer, and on SUCCESS the + // down-chain just wrote the new instance handle through it. + let inst = unsafe { *p_inst }; + // SAFETY: `inst` is the live instance created above; `next_gipa` resolves its + // instance-level commands, and every Fn* typedef used here mirrors the prototype of the + // exact command name it is resolved from. + let data = unsafe { + InstanceData { + instance: inst, + next_gipa, + next_gpdpa, + destroy_instance: resolve(next_gipa, inst, c"vkDestroyInstance"), + get_surface_formats: resolve(next_gipa, inst, c"vkGetPhysicalDeviceSurfaceFormatsKHR"), + get_surface_formats2: resolve( + next_gipa, + inst, + c"vkGetPhysicalDeviceSurfaceFormats2KHR", + ), + create_win32_surface: resolve(next_gipa, inst, c"vkCreateWin32SurfaceKHR"), + destroy_surface: resolve(next_gipa, inst, c"vkDestroySurfaceKHR"), + } }; if let Ok(mut g) = instances().lock() { - g.insert(key(inst.as_raw()), data); + // SAFETY: `inst` is the live dispatchable handle created above — its first word is the + // dispatch key. + g.insert(unsafe { key(inst.as_raw()) }, data); } log("create_instance: hooked"); vk::Result::SUCCESS @@ -602,26 +764,40 @@ unsafe extern "system" fn destroy_instance(inst: vk::Instance, p_alloc: *const c let data = instances() .lock() .ok() - .and_then(|mut g| g.remove(&key(inst.as_raw()))); + // SAFETY: `inst` is non-null, and vkDestroyInstance requires a live instance handle — + // still live during this call — whose first word is the dispatch key. + .and_then(|mut g| g.remove(&unsafe { key(inst.as_raw()) })); if let Some(d) = data { if let Some(f) = d.destroy_instance { - f(inst, p_alloc); + // SAFETY: `f` is the down-chain vkDestroyInstance resolved for this very instance at + // create time; forwarding the caller's own arguments unchanged. + unsafe { f(inst, p_alloc) }; } } } // ---- device chain (pass-through; keeps device-level dispatch working) ---- +/// Walk `p_ci`'s pNext chain for the loader's device layer-link node. +/// +/// # Safety +/// Same contract as [`find_instance_link`]: `p_ci` must point to a valid create-info struct +/// whose `pNext` chain is a well-formed list of `{sType, pNext}`-headed nodes. unsafe fn find_device_link(p_ci: *const c_void) -> *mut LayerDeviceCreateInfo { - let mut node = (*(p_ci as *const BaseIn)).p_next as *const BaseIn; - while !node.is_null() { - if (*node).s_type.as_raw() == LOADER_DEVICE_CREATE_INFO { - let lci = node as *mut LayerDeviceCreateInfo; - if (*lci).function == VK_LAYER_LINK_INFO { - return lci; + // SAFETY: per this function's contract, `p_ci` and every non-null `pNext` reached from it + // point at nodes beginning with a {sType, pNext} header; a node whose sType says it is the + // loader's device create-info really is a LayerDeviceCreateInfo (the loader wrote it). + unsafe { + let mut node = (*(p_ci as *const BaseIn)).p_next as *const BaseIn; + while !node.is_null() { + if (*node).s_type.as_raw() == LOADER_DEVICE_CREATE_INFO { + let lci = node as *mut LayerDeviceCreateInfo; + if (*lci).function == VK_LAYER_LINK_INFO { + return lci; + } } + node = (*node).p_next as *const BaseIn; } - node = (*node).p_next as *const BaseIn; } ptr::null_mut() } @@ -632,35 +808,54 @@ unsafe extern "system" fn create_device( p_alloc: *const c_void, p_dev: *mut vk::Device, ) -> vk::Result { - let lci = find_device_link(p_ci); + // SAFETY: the loader invokes this hook with a valid VkDeviceCreateInfo whose pNext chain it + // built — the chain find_device_link's contract requires. + let lci = unsafe { find_device_link(p_ci) }; if lci.is_null() { return vk::Result::ERROR_INITIALIZATION_FAILED; } - let link = (*lci).u; - if link.is_null() { - return vk::Result::ERROR_INITIALIZATION_FAILED; - } - let next_gipa = (*link).next_gipa; - let next_gdpa = (*link).next_gdpa; - (*lci).u = (*link).p_next; + // SAFETY: `lci` is non-null and points into the loader's live chain. Its `u` link (checked + // non-null before use) is this layer's link entry; reading the down-chain pointers and + // advancing `(*lci).u` is the layer protocol, exactly as in create_instance. + let (next_gipa, next_gdpa) = unsafe { + let link = (*lci).u; + if link.is_null() { + return vk::Result::ERROR_INITIALIZATION_FAILED; + } + let gipa = (*link).next_gipa; + let gdpa = (*link).next_gdpa; + (*lci).u = (*link).p_next; + (gipa, gdpa) + }; let inst = instances() .lock() .ok() - .and_then(|g| g.get(&key(pdev.as_raw())).map(|d| d.instance)) + // SAFETY: vkCreateDevice requires `pdev` to be a live physical-device handle — a + // dispatchable object sharing its instance's dispatch table, so its first word is the + // same dispatch key create_instance stored. + .and_then(|g| g.get(&unsafe { key(pdev.as_raw()) }).map(|d| d.instance)) .unwrap_or(vk::Instance::null()); - let create: FnCreateDevice = match resolve(next_gipa, inst, c"vkCreateDevice") { + // SAFETY: `next_gipa` is the loader-supplied down-chain GIPA for this create call, `inst` is + // the (possibly null) instance owning `pdev`, and FnCreateDevice mirrors vkCreateDevice's + // prototype. + let create: FnCreateDevice = match unsafe { resolve(next_gipa, inst, c"vkCreateDevice") } { Some(f) => f, None => return vk::Result::ERROR_INITIALIZATION_FAILED, }; - let res = create(pdev, p_ci, p_alloc, p_dev); + // SAFETY: forwarding the loader's own arguments unchanged to the down-chain create. + let res = unsafe { create(pdev, p_ci, p_alloc, p_dev) }; if res != vk::Result::SUCCESS { return res; } - let dev = *p_dev; + // SAFETY: vkCreateDevice requires pDevice to be a valid pointer, and on SUCCESS the + // down-chain just wrote the new device handle through it. + let dev = unsafe { *p_dev }; if let Ok(mut g) = devices().lock() { - g.insert(key(dev.as_raw()), DeviceData { next_gdpa }); + // SAFETY: `dev` is the live dispatchable handle created above — its first word is the + // dispatch key. + g.insert(unsafe { key(dev.as_raw()) }, DeviceData { next_gdpa }); } vk::Result::SUCCESS } @@ -674,19 +869,28 @@ unsafe extern "system" fn create_win32_surface( p_surface: *mut vk::SurfaceKHR, ) -> vk::Result { let down = instances().lock().ok().and_then(|g| { - g.get(&key(inst.as_raw())) + // SAFETY: vkCreateWin32SurfaceKHR requires `inst` to be a live instance handle — a + // dispatchable object whose first word is the dispatch key. + g.get(&unsafe { key(inst.as_raw()) }) .and_then(|d| d.create_win32_surface) }); let down = match down { Some(f) => f, None => return vk::Result::ERROR_EXTENSION_NOT_PRESENT, }; - let res = down(inst, p_ci, p_alloc, p_surface); + // SAFETY: `down` is the down-chain vkCreateWin32SurfaceKHR resolved for this instance at + // create time; forwarding the caller's own arguments unchanged. + let res = unsafe { down(inst, p_ci, p_alloc, p_surface) }; if res == vk::Result::SUCCESS { - // VkWin32SurfaceCreateInfoKHR: sType@0, pNext@8, flags@16, hinstance@24, hwnd@32 - let hwnd = *((p_ci as *const u8).add(32) as *const isize); + // SAFETY: the down-chain call succeeded, so `p_ci` pointed at a valid + // VkWin32SurfaceCreateInfoKHR for the whole call (the ICD just consumed it). On x86_64 + // its layout is sType(4+4 pad)@0, pNext@8, flags(4+4 pad)@16, hinstance@24, hwnd@32 — + // so `p_ci + 32` is an in-bounds, 8-aligned read of the HWND field. + let hwnd = unsafe { *((p_ci as *const u8).add(32) as *const isize) }; if let Ok(mut m) = surface_hwnds().lock() { - m.insert((*p_surface).as_raw(), hwnd); + // SAFETY: vkCreateWin32SurfaceKHR requires pSurface to be a valid pointer, and on + // SUCCESS the down-chain just wrote the new surface handle through it. + m.insert(unsafe { *p_surface }.as_raw(), hwnd); } } res @@ -700,12 +904,16 @@ unsafe extern "system" fn destroy_surface( if let Ok(mut m) = surface_hwnds().lock() { m.remove(&surface.as_raw()); } - let down = instances() - .lock() - .ok() - .and_then(|g| g.get(&key(inst.as_raw())).and_then(|d| d.destroy_surface)); + let down = instances().lock().ok().and_then(|g| { + // SAFETY: vkDestroySurfaceKHR requires `inst` to be a live instance handle — a + // dispatchable object whose first word is the dispatch key. + g.get(&unsafe { key(inst.as_raw()) }) + .and_then(|d| d.destroy_surface) + }); if let Some(f) = down { - f(inst, surface, p_alloc); + // SAFETY: `f` is the down-chain vkDestroySurfaceKHR resolved for this instance at + // create time; forwarding the caller's own arguments unchanged. + unsafe { f(inst, surface, p_alloc) }; } } @@ -718,7 +926,9 @@ unsafe extern "system" fn get_surface_formats( p_formats: *mut vk::SurfaceFormatKHR, ) -> vk::Result { let down = instances().lock().ok().and_then(|g| { - g.get(&key(pdev.as_raw())) + // SAFETY: vkGetPhysicalDeviceSurfaceFormatsKHR requires `pdev` to be a live + // physical-device handle — a dispatchable object whose first word is the dispatch key. + g.get(&unsafe { key(pdev.as_raw()) }) .and_then(|d| d.get_surface_formats) }); let down = match down { @@ -727,13 +937,17 @@ unsafe extern "system" fn get_surface_formats( }; let mut n = 0u32; - let r = down(pdev, surface, &mut n, ptr::null_mut()); + // SAFETY: count-query form of the two-call idiom — null pFormats plus a count out-pointer + // backed by the live local `n`; the caller's pdev/surface are forwarded unchanged. + let r = unsafe { down(pdev, surface, &mut n, ptr::null_mut()) }; if r != vk::Result::SUCCESS { return r; } let mut real = vec![vk::SurfaceFormatKHR::default(); n as usize]; if n > 0 { - let r = down(pdev, surface, &mut n, real.as_mut_ptr()); + // SAFETY: `real` holds exactly `n` elements and `n` is passed as the in/out capacity, so + // the down-chain writes at most `n` entries and shrinks `n` to what it wrote. + let r = unsafe { down(pdev, surface, &mut n, real.as_mut_ptr()) }; if r != vk::Result::SUCCESS { return r; } @@ -753,16 +967,24 @@ unsafe extern "system" fn get_surface_formats( } if p_formats.is_null() { - *p_count = aug.len() as u32; + // SAFETY: vkGetPhysicalDeviceSurfaceFormatsKHR requires pSurfaceFormatCount to be a + // valid u32 pointer in both call forms. + unsafe { *p_count = aug.len() as u32 }; return vk::Result::SUCCESS; } - let m = (*p_count as usize).min(aug.len()); - ptr::copy_nonoverlapping(aug.as_ptr(), p_formats, m); - *p_count = m as u32; - if m < aug.len() { - vk::Result::INCOMPLETE - } else { - vk::Result::SUCCESS + // SAFETY: with a non-null pSurfaceFormats the spec requires *pSurfaceFormatCount to be + // readable and pSurfaceFormats to point at at least that many elements; `m` is clamped to + // both that capacity and our own length, and the caller's buffer cannot overlap the + // freshly-allocated `aug`. + unsafe { + let m = (*p_count as usize).min(aug.len()); + ptr::copy_nonoverlapping(aug.as_ptr(), p_formats, m); + *p_count = m as u32; + if m < aug.len() { + vk::Result::INCOMPLETE + } else { + vk::Result::SUCCESS + } } } @@ -773,7 +995,9 @@ unsafe extern "system" fn get_surface_formats2( p_formats: *mut c_void, ) -> vk::Result { let down = instances().lock().ok().and_then(|g| { - g.get(&key(pdev.as_raw())) + // SAFETY: vkGetPhysicalDeviceSurfaceFormats2KHR requires `pdev` to be a live + // physical-device handle — a dispatchable object whose first word is the dispatch key. + g.get(&unsafe { key(pdev.as_raw()) }) .and_then(|d| d.get_surface_formats2) }); let down = match down { @@ -782,7 +1006,10 @@ unsafe extern "system" fn get_surface_formats2( }; let mut n = 0u32; - let r = down(pdev, p_info, &mut n, ptr::null_mut()); + // SAFETY: count-query form of the two-call idiom — null pSurfaceFormats plus a count + // out-pointer backed by the live local `n`; the caller's pdev/pSurfaceInfo are forwarded + // unchanged (the spec requires pSurfaceInfo to stay valid for the call). + let r = unsafe { down(pdev, p_info, &mut n, ptr::null_mut()) }; if r != vk::Result::SUCCESS { return r; } @@ -794,15 +1021,21 @@ unsafe extern "system" fn get_surface_formats2( }) .collect(); if n > 0 { - let r = down(pdev, p_info, &mut n, real.as_mut_ptr() as *mut c_void); + // SAFETY: `real` holds exactly `n` properly-stamped VkSurfaceFormat2KHR mirrors + // (SurfaceFormat2Raw layout-asserted at the type), and `n` is passed as the in/out + // capacity, so the down-chain writes at most `n` entries. + let r = unsafe { down(pdev, p_info, &mut n, real.as_mut_ptr() as *mut c_void) }; if r != vk::Result::SUCCESS { return r; } } real.truncate(n as usize); - // VkPhysicalDeviceSurfaceInfo2KHR: sType@0, pNext@8, surface@16 - let surface = vk::SurfaceKHR::from_raw(*((p_info as *const u8).add(16) as *const u64)); + // SAFETY: the spec requires pSurfaceInfo to point at a valid VkPhysicalDeviceSurfaceInfo2KHR + // for the whole call. On x86_64 its layout is sType(4+4 pad)@0, pNext@8, surface(u64)@16 — + // so `p_info + 16` is an in-bounds, 8-aligned read of the non-dispatchable surface handle. + let surface = + vk::SurfaceKHR::from_raw(unsafe { *((p_info as *const u8).add(16) as *const u64) }); let mut extras: Vec = Vec::new(); if !real.is_empty() && should_inject(surface) { @@ -817,25 +1050,34 @@ unsafe extern "system" fn get_surface_formats2( let total = real.len() + extras.len(); if p_formats.is_null() { - *p_count = total as u32; + // SAFETY: vkGetPhysicalDeviceSurfaceFormats2KHR requires pSurfaceFormatCount to be a + // valid u32 pointer in both call forms. + unsafe { *p_count = total as u32 }; return vk::Result::SUCCESS; } - let m = (*p_count as usize).min(total); - let out = p_formats as *mut SurfaceFormat2Raw; - for i in 0..m { - let sf = if i < real.len() { - real[i].surface_format + // SAFETY: with a non-null pSurfaceFormats the spec requires *pSurfaceFormatCount to be + // readable and pSurfaceFormats to point at at least that many VkSurfaceFormat2KHR — whose + // layout SurfaceFormat2Raw mirrors (asserted at the type). `m` is clamped to both bounds. + // Only sType and surfaceFormat are stamped per element; each element's pNext chain is left + // exactly as the caller built it. + unsafe { + let m = (*p_count as usize).min(total); + let out = p_formats as *mut SurfaceFormat2Raw; + for i in 0..m { + let sf = if i < real.len() { + real[i].surface_format + } else { + extras[i - real.len()] + }; + let dst = out.add(i); + (*dst).s_type = vk::StructureType::SURFACE_FORMAT_2_KHR; + (*dst).surface_format = sf; + } + *p_count = m as u32; + if m < total { + vk::Result::INCOMPLETE } else { - extras[i - real.len()] - }; - let dst = out.add(i); - (*dst).s_type = vk::StructureType::SURFACE_FORMAT_2_KHR; - (*dst).surface_format = sf; - } - *p_count = m as u32; - if m < total { - vk::Result::INCOMPLETE - } else { - vk::Result::SUCCESS + vk::Result::SUCCESS + } } } diff --git a/scripts/ci/check-unsafe-hygiene.sh b/scripts/ci/check-unsafe-hygiene.sh new file mode 100755 index 00000000..3fa03d4c --- /dev/null +++ b/scripts/ci/check-unsafe-hygiene.sh @@ -0,0 +1,211 @@ +#!/bin/sh +# Unsafe-hygiene grep gates (rust-safety programme §4 WP2c). Three classes no lint covers: +# +# A. `unsafe fn` whose body contains no unsafe operation. Because the workspaces deny +# `unsafe_op_in_unsafe_fn`, every real unsafe op inside an `unsafe fn` sits in an explicit +# `unsafe {}` block — so an `unsafe fn` with no `unsafe` in its body is a marker carrying no +# contract (db659809 found two by hand, with call-site SAFETY proofs describing FFI the fns +# no longer performed). A contract-DEFERRING fn (`set_len` shape: safe body, the danger is in +# later safe code trusting the argument) is legitimate — waive it with a comment line +# `// unsafe-fn-no-op-ok: ` above the fn (doc lines +# may sit between). Two classes are skipped structurally: files carrying +# `#![allow(unsafe_op_in_unsafe_fn)]` (the fenced GPU/FFI backends — there the premise that +# ops are forced into blocks does not hold), and `unsafe extern "ABI" fn` definitions (loader +# / framework callbacks, where the unsafe marker is dictated by the PFN type they must match, +# not by a caller contract; gate B still covers their bodies). +# +# B. `unwrap`/`expect`/`panic!` inside an `extern "C"` / `extern "system"` fn body. Panic across +# an `extern` boundary is an abort since Rust 1.81 — not a diagnostic, not a sanitizer +# finding, not fuzzable (8b98d0b3: an ETW callback's `RING.lock().unwrap()` aborted the host +# on a poisoned lock). A body that routes through `catch_unwind` (the abi.rs pattern) is +# exempt; otherwise waive a deliberate abort with `// panic-in-extern-ok: ` directly +# above the fn. +# +# C. Safe-but-process-global APIs: `env::set_var`/`remove_var`, `sigaction`, `setlocale`, +# `set_current_dir`. Each is safe to call and unsound (or racy) from a live multithreaded +# process — the 972af299 environ data race lived in a file with ZERO occurrences of the word +# `unsafe`, invisible to the census. Edition 2024 makes `env::set_var` unsafe; until that +# migration this count-ratchet is the control. The baseline below enumerates today's debt +# per file; ANY increase (or a new file) fails. Shrink a file's count? Lower its baseline in +# the same commit. +# +# All three gates were shown to FAIL on deliberately planted instances before being made blocking +# (the gate-of-the-gate rule that caught cd72f77a's `0 * SLOT`). +# +# Textual gates, so textual limits: string literals containing `unsafe {` and macro-generated fns +# are invisible; nested `unsafe fn` items inside another fn's body attribute their blocks to the +# outer fn. Both classes are rare here and covered by review. + +set -u +cd "$(dirname "$0")/../.." || exit 2 + +fail=0 +tmp="${TMPDIR:-/tmp}/unsafe-hygiene.$$" +mkdir -p "$tmp" +trap 'rm -rf "$tmp"' EXIT + +# Tracked, non-vendored Rust sources. +git ls-files '*.rs' | grep -v '/vendor/' > "$tmp/files" + +# ---------------------------------------------------------------- gate A +awk ' +function reset() { state = 0; has_unsafe = 0; depth = 0 } +FNR == 1 { reset(); fenced = 0; waive_next = 0 } +/^#!\[allow\(unsafe_op_in_unsafe_fn\)\]$/ { fenced = 1 } +{ + line = $0 + sub(/^[ \t]+/, "", line) + is_comment = (line ~ /^\/\//) + if (is_comment && line ~ /unsafe-fn-no-op-ok:/) { waive_next = 1 } +} +fenced { next } +# fn-definition start: a plain `unsafe fn` outside a comment — not a type alias, not an +# `unsafe extern "ABI" fn` (signature-mandated markers; see the header) +state == 0 && !is_comment && /(^|[ \t(])unsafe[ \t]+fn[ \t]+[A-Za-z_]/ \ + && $0 !~ /^[ \t]*type[ \t]/ && $0 !~ /=[ \t]*unsafe/ { + state = 1; sig_file = FILENAME; sig_line = FNR + name = $0; sub(/.*fn[ \t]+/, "", name); sub(/[^A-Za-z0-9_].*/, "", name) + waived = waive_next +} +state == 1 { + # declaration (trait method / extern block) ends before a body opens + if ($0 ~ /;/ && $0 !~ /{/) { reset(); next } + if ($0 ~ /{/) { + state = 2 + # count braces via gsub (returns the count, leaves the line unchanged) — the + # empty-separator split() alternative is a gawk extension mawk lacks + t = $0; opens = gsub(/\{/, "{", t); t = $0; closes = gsub(/\}/, "}", t) + depth = opens - closes + if (opens > 0 && depth == 0) { # one-line body + if ($0 ~ /unsafe[ \t]*{[^}]*}[^}]*}/ || $0 ~ /unsafe impl/) has_unsafe = 1 + if (!has_unsafe && !waived) { print sig_file ":" sig_line ": unsafe fn `" name "` has no unsafe operation in its body"; bad = 1 } + reset() + } + next + } + next +} +state == 2 { + if (!is_comment && ($0 ~ /unsafe[ \t]*{/ || $0 ~ /unsafe impl/)) has_unsafe = 1 + t = $0; opens = gsub(/\{/, "{", t); t = $0; closes = gsub(/\}/, "}", t) + depth += opens - closes + if (depth <= 0) { + if (!has_unsafe && !waived) { print sig_file ":" sig_line ": unsafe fn `" name "` has no unsafe operation in its body"; bad = 1 } + reset() + } +} +!is_comment { waive_next = 0 } +END { exit bad ? 1 : 0 } +' $(cat "$tmp/files") > "$tmp/gate_a" 2>&1 +if [ -s "$tmp/gate_a" ]; then + echo "GATE A — unsafe fn markers carrying no contract (waive a contract-deferring fn with" + echo " '// unsafe-fn-no-op-ok: ' on the line above):" + cat "$tmp/gate_a" + fail=1 +fi + +# ---------------------------------------------------------------- gate B +awk ' +function reset() { state = 0; depth = 0; guarded = 0; nhit = 0 } +FNR == 1 { reset(); waive_next = 0 } +{ + line = $0 + sub(/^[ \t]+/, "", line) + is_comment = (line ~ /^\/\//) + if (is_comment && line ~ /panic-in-extern-ok:/) { waive_next = 1 } +} +state == 0 && !is_comment && /extern[ \t]+"(C|system)"[ \t]+fn[ \t]+[A-Za-z_]/ \ + && $0 !~ /^[ \t]*type[ \t]/ && $0 !~ /=[ \t]*(unsafe[ \t]+)?extern/ { + state = 1; sig_file = FILENAME; sig_line = FNR + name = $0; sub(/.*fn[ \t]+/, "", name); sub(/[^A-Za-z0-9_].*/, "", name) + waived = waive_next +} +state == 1 { + if ($0 ~ /;/ && $0 !~ /{/) { reset(); next } + if ($0 ~ /{/) { + state = 2 + t = $0; opens = gsub(/\{/, "{", t); t = $0; closes = gsub(/\}/, "}", t) + depth = opens - closes + if (depth == 0) reset() + next + } + next +} +state == 2 { + if ($0 ~ /catch_unwind/) guarded = 1 + if (!is_comment && ($0 ~ /\.unwrap\(\)/ || $0 ~ /\.expect\(/ || $0 ~ /(^|[^a-zA-Z0-9_])panic!/)) { + nhit++; hitline[nhit] = FILENAME ":" FNR + } + t = $0; opens = gsub(/\{/, "{", t); t = $0; closes = gsub(/\}/, "}", t) + depth += opens - closes + if (depth <= 0) { + if (nhit > 0 && !guarded && !waived) { + for (i = 1; i <= nhit; i++) + print hitline[i] ": unwrap/expect/panic! reachable in extern fn `" name "` (no catch_unwind)" + bad = 1 + } + reset() + } +} +!is_comment { waive_next = 0 } +END { exit bad ? 1 : 0 } +' $(cat "$tmp/files") > "$tmp/gate_b" 2>&1 +if [ -s "$tmp/gate_b" ]; then + echo "GATE B — panic across an extern boundary aborts the process since Rust 1.81. Route the" + echo " body through catch_unwind (see punktfunk-core abi.rs) or waive a deliberate" + echo " abort with '// panic-in-extern-ok: ' on the line above the fn:" + cat "$tmp/gate_b" + fail=1 +fi + +# ---------------------------------------------------------------- gate C +# Baseline: per-file count of process-global-API mentions (call sites AND comments — the grep is +# the contract; keep it dumb and stable). Regenerate a line with: +# grep -c 'env::set_var\|env::remove_var\|sigaction\|setlocale\|set_current_dir' +cat > "$tmp/gate_c_baseline" <<'BASELINE' +clients/linux/src/app.rs:1 +clients/linux/src/spawn.rs:1 +clients/session/src/main.rs:4 +crates/pf-console-ui/src/screens/settings.rs:1 +crates/pf-console-ui/src/shell/tests.rs:2 +crates/pf-encode/src/enc/linux/nvenc_cuda.rs:49 +crates/pf-encode/src/enc/linux/worker.rs:1 +crates/pf-encode/src/enc/windows/nvenc.rs:4 +crates/pf-inject/src/inject/linux/steam_gadget.rs:5 +crates/pf-vdisplay/src/lib.rs:1 +crates/pf-vdisplay/src/vdisplay/routing.rs:4 +crates/pf-vdisplay/src/vdisplay/session.rs:10 +crates/pf-vkdecode/tests/common/mod.rs:1 +crates/pf-vkdecode/tests/gpu_parity.rs:5 +crates/pf-win-display/src/win_display.rs:2 +crates/punktfunk-core/src/quic/endpoint.rs:2 +crates/punktfunk-host/src/identity.rs:3 +crates/punktfunk-host/src/library/art.rs:4 +crates/punktfunk-host/src/mgmt/tests.rs:3 +crates/punktfunk-host/src/native.rs:4 +crates/punktfunk-host/src/windows/service.rs:1 +BASELINE + +: > "$tmp/gate_c" +while IFS= read -r f; do + n=$(grep -c 'env::set_var\|env::remove_var\|sigaction\|setlocale\|set_current_dir' "$f") + [ "$n" -eq 0 ] && continue + base=$(grep -F "$f:" "$tmp/gate_c_baseline" | head -1 | awk -F: '{print $NF}') + base=${base:-0} + if [ "$n" -gt "$base" ]; then + echo "$f: $n process-global-API mentions (baseline $base)" >> "$tmp/gate_c" + fi +done < "$tmp/files" +if [ -s "$tmp/gate_c" ]; then + echo "GATE C — env::set_var/remove_var, sigaction, setlocale, set_current_dir are safe to" + echo " call and unsound from a live multithreaded process (972af299). Fix the new" + echo " call site (a per-call env override belongs in Command::env; a handler install" + echo " belongs behind Once at startup) rather than raising the baseline:" + cat "$tmp/gate_c" + fail=1 +fi + +if [ "$fail" -eq 0 ]; then + echo "unsafe-hygiene: all three gates clean" +fi +exit "$fail" diff --git a/tools/display-disturb/src/main.rs b/tools/display-disturb/src/main.rs index 83556808..6ed80cc6 100644 --- a/tools/display-disturb/src/main.rs +++ b/tools/display-disturb/src/main.rs @@ -20,7 +20,6 @@ //! `display-disturb modeset [--interval-ms 2000]` // Unsafe-proof program: every `unsafe {}` in this tool carries a `// SAFETY:` proof. -#![deny(clippy::undocumented_unsafe_blocks)] #[cfg(not(target_os = "windows"))] fn main() {