chore(unsafe): the workspace adopts the drivers' unsafe discipline
`packaging/windows/drivers/*` has run `deny(unsafe_op_in_unsafe_fn)` +
`deny(clippy::undocumented_unsafe_blocks)` for a while, with `forbid(unsafe_code)`
on the modules that need no unsafe at all. The main workspace had no lint config
whatsoever, so nothing stopped a clean crate from quietly growing an `unsafe`, and
nothing distinguished the handful of genuinely-unsafe lines inside a 600-line
`unsafe fn` from the safe ones surrounding them.
Three things, all mechanical:
* `#![forbid(unsafe_code)]` on the eight crates that already contain zero unsafe
(`pf-driver-proto`, `pf-host-config`, `pf-paths`, the three clean clients, both
tools). These were clean by accident, not by contract; now they are clean by
contract.
* `unsafe_op_in_unsafe_fn = "warn"` workspace-wide. `unsafe fn` states a contract
the CALLER must uphold — it was never meant to switch off checking for the whole
body. Measured fallout is 300 sites on Linux, and they are concentrated: six
files carry all of them, while `punktfunk-core`, `pf-frame`, `pf-clipboard` and
`pf-vdisplay` are already at zero. `warn` (not `deny`) so the build stays green
while those six are worked down; it flips to `deny` once they are. This is also
the Rust 2024 default, so it pays off the edition migration early.
* `proc::current_uid()` replaces eight `unsafe { libc::getuid() }` blocks. Each
site had copied out the same SAFETY note verbatim, which is the tell: `getuid()`
is parameterless, always succeeds and touches no memory, so there is no contract
for a caller to uphold and no reason for the unsafe to be visible eight times.
One `unsafe` behind a safe wrapper, none at the call sites.
Verified: `pf-vdisplay` builds clean on Linux (Nobara) at zero E0133; the
macOS-buildable crates build clean locally. No behaviour change.
This commit is contained in:
+10
@@ -56,6 +56,16 @@ license = "MIT OR Apache-2.0"
|
||||
authors = ["unom"]
|
||||
repository = "https://git.unom.io/unom/punktfunk"
|
||||
|
||||
# The `unsafe` discipline the `packaging/windows/drivers/*` crates already run, extended to the
|
||||
# workspace. `unsafe fn` marks a CONTRACT the caller must uphold; it is not a licence for the whole
|
||||
# body to skip checking. Without this lint an `unsafe fn` body is unchecked end to end, so a 600-line
|
||||
# function hides which handful of lines are actually the unsafe ones — exactly the reviewer-hostile
|
||||
# shape we are working down. `warn` while the remaining sites are cleared crate by crate; flip to
|
||||
# `deny` once they are, so it can never regress. (This is the Rust 2024 default; adopting it early
|
||||
# also pays off the edition migration.)
|
||||
[workspace.lints.rust]
|
||||
unsafe_op_in_unsafe_fn = "warn"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "thin"
|
||||
|
||||
@@ -64,3 +64,6 @@ libc = "0.2"
|
||||
# host + Linux client use. audiopus_sys vendors libopus (pure C) and builds it static via cmake —
|
||||
# the cargo-ndk build sets LIBOPUS_STATIC=1/LIBOPUS_NO_PKG=1 so it links the bundled lib, not the host's.
|
||||
opus = "0.3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -32,3 +32,6 @@ anyhow = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
relm4 = { version = "0.11", features = ["libadwaita"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Hosts, pairing/trust, settings, and the desktop library page; every stream (and the
|
||||
//! console game library) runs in the spawned `punktfunk-session` Vulkan binary — the
|
||||
//! shell never touches video (punktfunk-planning `linux-client-rearchitecture.md`).
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
// The UI-agnostic plumbing lives in `pf-client-core`, shared with the session binary.
|
||||
// Root re-exports keep every `crate::trust`-style path resolving unchanged.
|
||||
|
||||
@@ -22,3 +22,6 @@ mdns-sd = "0.20"
|
||||
# encoder. libopus is already in the graph via `punktfunk-core`'s quic feature; this exposes the
|
||||
# name directly. Cross-platform (cmake-vendored), so the probe builds + validates everywhere.
|
||||
opus = "0.3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
//! [--input-test | --mic-test [--mic-burst] | --touch-test | --rich-input-test]
|
||||
//! [--pin HEX | --pair PIN] [--compositor NAME] [--gamepad NAME] | --discover [SECS]`
|
||||
//! Env: `PUNKTFUNK_CLIENT_10BIT=1` / `PUNKTFUNK_CLIENT_444=1` advertise the 10-bit / 4:4:4 caps.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use punktfunk_core::config::GamepadPref;
|
||||
|
||||
@@ -45,3 +45,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
# the SDK); same pattern as clients/windows.
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winresource = "0.1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
//! the first presented frame, `stats:` lines per 1 s window, one `{"error": …}` /
|
||||
//! `{"ended": …}` JSON line on the way out. Logs go to stderr. Exit codes: 0 clean end,
|
||||
//! 2 connect failed, 3 trust rejected / pairing required, 4 presenter init failed.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
#[cfg(all(any(target_os = "linux", windows), feature = "ui"))]
|
||||
mod console;
|
||||
|
||||
@@ -95,3 +95,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winresource = "0.1"
|
||||
windows-reactor-setup = { git = "https://github.com/microsoft/windows-rs", rev = "a4f7b2cb7c63c6bb7fc77a2affe57145be1d8c4f" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -15,3 +15,6 @@ links = "vpl"
|
||||
cmake = "0.1"
|
||||
# Same bindgen configuration as pyrowave-sys (runtime = dlopen libclang).
|
||||
bindgen = { version = "0.72", features = ["runtime"], default-features = false }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -58,3 +58,6 @@ windows = { version = "0.62", features = [
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -79,3 +79,6 @@ windows = { git = "https://github.com/microsoft/windows-rs", rev = "a4f7b2cb7c63
|
||||
# still strictly per-session opt-in (Settings codec pick / PUNKTFUNK_PREFER_PYROWAVE=1).
|
||||
default = ["pyrowave"]
|
||||
pyrowave = ["dep:pyrowave-sys", "dep:ash"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -52,3 +52,6 @@ windows = { version = "0.62", features = [
|
||||
"Win32_System_Ole",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -33,3 +33,6 @@ sdl3 = { version = "0.18", features = ["hidapi", "ash"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
sdl3 = { version = "0.18", features = ["hidapi", "ash", "build-from-source"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -18,3 +18,6 @@ publish = false
|
||||
[dependencies]
|
||||
# `min_const_generics`: Pod/Zeroable for `[u8; N]` of any N (the gamepad SHM reserved tails are >32).
|
||||
bytemuck = { version = "1.19", features = ["derive", "min_const_generics"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//!
|
||||
//! The GUID and LUID are carried as plain integers; the host converts to `windows::core::GUID` /
|
||||
//! `windows::Win32::Foundation::LUID` and the driver to its own bindgen types via the same constants.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![cfg_attr(not(test), no_std)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
@@ -92,3 +92,6 @@ pyrowave = ["dep:pyrowave-sys"]
|
||||
# (design/native-qsv-encoder.md). ⚠ Like `nvenc`: hand builds need this feature or
|
||||
# Intel boxes fall through to the ffmpeg path / software.
|
||||
qsv = ["dep:libvpl-sys"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -20,3 +20,6 @@ ash = { version = "0.38", features = ["loaded"] }
|
||||
# Same bindgen configuration as ffmpeg-sys-next (runtime = dlopen libclang).
|
||||
bindgen = { version = "0.72", features = ["runtime"], default-features = false }
|
||||
pkg-config = "0.3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -35,3 +35,6 @@ windows = { version = "0.62", features = [
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -26,3 +26,6 @@ windows = { version = "0.62", features = [
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -10,3 +10,6 @@ description = "Process-wide punktfunk host configuration (env-parsed HostConfig
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
//! = off, anything else = on, so the old presence-style `=1` keeps working). The Linux `zerocopy`
|
||||
//! module keeps its own *truthy* parser (`1|true|yes|on`) — the two are independent features that
|
||||
//! share a name; do NOT conflate them.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
|
||||
@@ -75,3 +75,6 @@ windows = { version = "0.62", features = [
|
||||
"Win32_UI_Input_Pointer",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -11,3 +11,6 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! - [`config_dir`] resolves the per-host config directory (XDG / `%ProgramData%`, `PUNKTFUNK_CONFIG_DIR` override).
|
||||
//! - [`create_private_dir`] makes it owner-private (0700 / restrictive DACL).
|
||||
//! - [`write_secret_file`] writes an owner-only secret (0600 / SYSTEM+Admins DACL).
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -53,3 +53,6 @@ windows-sys = { version = "0.61", features = [
|
||||
# backend in pf-client-core; ON by default, matching pf-client-core's default.
|
||||
default = ["pyrowave"]
|
||||
pyrowave = ["pf-client-core/pyrowave"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -65,3 +65,6 @@ windows = { version = "0.62", features = [
|
||||
"Win32_System_IO",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -604,8 +604,7 @@ pub fn managed_session_available() -> bool {
|
||||
/// walking each candidate's ppid chain — so one client's nested gamescope never makes the next
|
||||
/// client attach to it.
|
||||
pub fn foreign_gamescope_running() -> bool {
|
||||
// SAFETY: `getuid()` is a parameterless POSIX call that always succeeds and touches no memory.
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let uid = crate::proc::current_uid();
|
||||
let our_pid = std::process::id();
|
||||
let Ok(entries) = std::fs::read_dir("/proc") else {
|
||||
return false;
|
||||
@@ -708,8 +707,7 @@ pub fn launch_into_session(cmd: &str) -> Result<std::process::Child> {
|
||||
/// when a process exposes it. Empty when no gamescope session is running / none exposes a `DISPLAY`.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn xwayland_cursor_targets() -> Vec<(String, Option<String>)> {
|
||||
// SAFETY: `getuid()` is a parameterless POSIX call that always succeeds and touches no memory.
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let uid = crate::proc::current_uid();
|
||||
let mut out: Vec<(String, Option<String>)> = Vec::new();
|
||||
let Ok(entries) = std::fs::read_dir("/proc") else {
|
||||
return out;
|
||||
@@ -765,8 +763,7 @@ pub(crate) fn xwayland_cursor_targets() -> Vec<(String, Option<String>)> {
|
||||
/// gamescope socket; `DISPLAY` is the nested Xwayland; `XAUTHORITY` is its auth file (for X
|
||||
/// clients that aren't gamescope children). Any one can be individually absent.
|
||||
fn discover_session_display_env() -> Option<(Option<String>, Option<String>, Option<String>)> {
|
||||
// SAFETY: `getuid()` is a parameterless POSIX call that always succeeds and touches no memory.
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let uid = crate::proc::current_uid();
|
||||
for e in std::fs::read_dir("/proc").ok()?.flatten() {
|
||||
let name = e.file_name();
|
||||
let Some(pid_str) = name.to_str() else {
|
||||
@@ -1441,8 +1438,7 @@ fn cgroup_under_user_manager(cgroup: &str) -> bool {
|
||||
|
||||
/// Our uid as a string — what `loginctl` wants for a user argument.
|
||||
fn uid_string() -> String {
|
||||
// SAFETY: `getuid()` is a parameterless POSIX call that always succeeds and touches no memory.
|
||||
unsafe { libc::getuid() }.to_string()
|
||||
crate::proc::current_uid().to_string()
|
||||
}
|
||||
|
||||
/// Is lingering on for this user (logind keeps the `--user` manager alive with no session)?
|
||||
|
||||
@@ -106,8 +106,7 @@ pub(crate) fn wait_for_steam_game_exit(
|
||||
/// `AppId=57` never matches appid 570) — specific to the game reaper, so Steam's own shader-precompile
|
||||
/// step (not reaper-wrapped) can't be mistaken for the game.
|
||||
fn steam_game_running(appid: u32) -> bool {
|
||||
// SAFETY: `getuid()` is a parameterless POSIX call that always succeeds and touches no memory.
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let uid = crate::proc::current_uid();
|
||||
let appid_tok = format!("AppId={appid}");
|
||||
let Ok(entries) = std::fs::read_dir("/proc") else {
|
||||
return false;
|
||||
|
||||
@@ -78,6 +78,21 @@ fn timed_out(cmd: &Command, budget: Duration) -> Error {
|
||||
)
|
||||
}
|
||||
|
||||
/// The calling process's real uid.
|
||||
///
|
||||
/// Every session/gamescope lookup that derives `/run/user/<uid>` (or filters `/proc` to "our"
|
||||
/// processes) needs this, and each one used to open its own `unsafe` block carrying a verbatim
|
||||
/// copy of the same SAFETY note. `getuid()` is parameterless, always succeeds, and touches no
|
||||
/// memory, so there is no contract for a caller to uphold — which makes it exactly the shape that
|
||||
/// belongs behind a safe wrapper instead of being restated at every call site. One `unsafe` here,
|
||||
/// none at the callers.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn current_uid() -> u32 {
|
||||
// SAFETY: parameterless POSIX call that always succeeds and touches no memory — it just
|
||||
// returns the calling process's real uid. Nothing is aliased, read, or freed.
|
||||
unsafe { libc::getuid() }
|
||||
}
|
||||
|
||||
// `unix` gate, not `test` alone: this module is compiled on every platform (lib.rs declares it
|
||||
// unconditionally), but the cases below spawn `sleep`/`true`/`echo` as EXECUTABLES. On Windows
|
||||
// `echo` is a shell builtin and there is no `sleep.exe`, so an ungated module turns a green suite
|
||||
|
||||
@@ -280,9 +280,7 @@ pub(crate) fn runtime_dir() -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
fn default_runtime_dir(env: &EnvProbe) -> String {
|
||||
env.xdg_runtime_dir.clone().unwrap_or_else(|| {
|
||||
// SAFETY: `getuid()` is a parameterless POSIX call that always succeeds and touches no
|
||||
// memory — it just returns the calling process's real uid. Nothing is aliased or freed.
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let uid = crate::proc::current_uid();
|
||||
format!("/run/user/{uid}")
|
||||
})
|
||||
}
|
||||
@@ -305,9 +303,7 @@ fn default_bus(env: &EnvProbe, runtime: &str) -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn detect_active_session() -> ActiveSession {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
// SAFETY: `getuid()` is a parameterless POSIX call that always succeeds and touches no memory —
|
||||
// it just returns the calling process's real uid. Nothing is aliased or freed.
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let uid = crate::proc::current_uid();
|
||||
// ONE sample of the session-scoped env, before any scanning — see [`EnvProbe`]. Everything
|
||||
// below reads this snapshot, never the process env.
|
||||
let env = EnvProbe::sample();
|
||||
@@ -712,8 +708,7 @@ mod tests {
|
||||
|
||||
impl FakeRuntime {
|
||||
fn new(tag: &str, pids: &[u32]) -> FakeRuntime {
|
||||
// SAFETY: parameterless POSIX call, returns the calling process's uid; touches no memory.
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let uid = crate::proc::current_uid();
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("pf-swaysock-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
|
||||
@@ -36,3 +36,6 @@ windows = { version = "0.62", features = [
|
||||
"Win32_System_StationsAndDesktops",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -34,3 +34,6 @@ serde_json = "1"
|
||||
# Linux-only like the code under test: the render-node scan is exercised against a fixture tree.
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -107,3 +107,6 @@ harness = false
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.29"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -310,3 +310,6 @@ pyrowave = ["pf-encode/pyrowave"]
|
||||
# builds of this crate never execute the winresource block).
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winresource = "0.1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -58,3 +58,6 @@ libc = "0.2"
|
||||
# Windows client's build.rs — cross-builds from Linux CI runners skip it.
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winresource = "0.1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -15,3 +15,6 @@ links = "pyrowave"
|
||||
cmake = "0.1"
|
||||
# Same bindgen configuration as pf-ffvk (runtime = dlopen libclang).
|
||||
bindgen = { version = "0.72", features = ["runtime"], default-features = false }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -14,3 +14,6 @@ windows = { version = "0.62", features = [
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Foundation",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -9,3 +9,6 @@ authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! anything, so regressions are quantifiable.
|
||||
//!
|
||||
//! Status: scaffold.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
fn main() {
|
||||
println!(
|
||||
|
||||
@@ -10,3 +10,6 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
punktfunk-core = { path = "../../crates/punktfunk-core" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! both FEC schemes, and prints how many frames survive. A pure-software stand-in for
|
||||
//! `tc netem` that needs no network and runs anywhere `punktfunk_core` builds. The real punktfunk/1
|
||||
//! harness adds `tc netem` jitter/reorder on the UDP path.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use punktfunk_core::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role};
|
||||
use punktfunk_core::crypto::SessionKey;
|
||||
|
||||
Reference in New Issue
Block a user