chore(safety): three unsafe-hygiene grep gates, blocking in ci.yml (WP2c gates)
scripts/ci/check-unsafe-hygiene.sh — textual gates for three classes no lint covers: A. unsafe fn markers carrying no contract. unsafe_op_in_unsafe_fn forces real ops into blocks, so an unsafe fn with no `unsafe` in its body is a marker with no contract (db659809found two by hand). Contract-deferring fns (Vec::set_len shape) waive with `// unsafe-fn-no-op-ok: <reason>`; fenced files and `unsafe extern "ABI" fn` (signature-mandated markers) are skipped structurally. B. unwrap/expect/panic! inside extern "C"/"system" bodies — an abort since Rust 1.81, not linted, not fuzzable (8b98d0b3). catch_unwind bodies are exempt; `// panic-in-extern-ok: <reason>` waives a deliberate abort. C. Safe-but-process-global APIs (env::set_var/remove_var, sigaction, setlocale, set_current_dir) — the972af299environ race lived in a file with zero occurrences of the word `unsafe`. Per-file count ratchet with the baseline in the script; any increase or new file fails. Making gate B clean on main surfaced 14 real instances of exactly its class — `.lock().unwrap()` in unguarded extern fns, where a poisoned mutex aborts the embedding process: six punktfunk-core abi.rs entry points (poll_frame, next_au, next_audio, next_audio_pcm, next_cursor_shape, next_clipboard), seven Android JNI entry points, and the Windows client's deeplink wnd_proc. All fixed with poison-recovering locks (the slots are last-value caches, valid whatever a poisoned writer left) and Option::insert for the set-then-unwrap shape; punktfunk-core's 203 lib tests pass. Gate A's findings were six genuine contract-deferring fns — waived with reasons, not fixed, because the markers are correct. Gate-of-the-gate: all three shown to FAIL on deliberately planted instances (marker fn, panicking extern callback, env::set_var in an unlisted file) and to run clean on the tree, before the ci.yml step made them blocking.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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<AddedMonitor> {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -53,6 +53,16 @@ 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.
|
||||
fn lock_recover<T>(m: &std::sync::Mutex<T>) -> 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 +481,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 +2258,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 +2322,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 +2474,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 +3079,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 +4059,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 +4071,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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user