openentropy-core 0.12.3

Core entropy harvesting library — hardware noise sources, raw or SHA-256 conditioned
Documentation
//! proc_info / proc_pid_rusage system call timing entropy.
//!
//! The `proc_pidinfo()` and `proc_pid_rusage()` system calls query the kernel's
//! process information subsystem. Each call must acquire the kernel's BSD process
//! lock (`proc_lock`), walk the process table, and collect the requested data.
//!
//! ## Physics
//!
//! The timing of these system calls varies based on:
//!
//! 1. **`proc_lock` contention**: Any concurrent fork/exec/exit/wait4 operation
//!    holds `proc_lock` exclusively. Our call must wait for the lock to be
//!    released, creating variable delay proportional to concurrent process
//!    lifecycle activity.
//!
//! 2. **CPU affinity and scheduler state**: The kernel thread handling our
//!    system call may be preempted or migrated between when we enter the kernel
//!    and when we return, adding scheduler jitter.
//!
//! 3. **Page fault cost for result struct**: If the kernel's result buffer or
//!    the process's task struct is not in L2/L3 cache (e.g., after a long idle
//!    period), the kernel must page-fault the data in.
//!
//! 4. **Hardware performance counter collection (rusage only)**: `proc_pid_rusage`
//!    with RUSAGE_INFO_V4 collects CPU cycle counts and memory bandwidth stats,
//!    requiring a cross-core hardware counter read that adds variable latency.
//!
//! Empirically on M4 Mac mini (N=1000):
//! - `proc_pidinfo(TBSDINFO)`:  mean=478.8 ticks, CV=47.7%, range=[434,7667]
//! - `proc_pid_rusage(V4)`:     mean=726.3 ticks, CV=43.0%, range=[666,10583]
//! - Both have LSB≈0.24–0.29 (near-uniform, unlike the "always even" cluster)
//!
//! ## Cross-Process Sensitivity
//!
//! This is a genuine cross-process covert channel: any process on the system
//! that creates/destroys processes, forks, or executes programs increases
//! `proc_lock` contention and extends our call duration. Terminal commands,
//! build systems, shell scripts, and browser tab management all leak into
//! our timing distribution.
//!
//! ## Why LSB≈0.24 (Near-Uniform)
//!
//! Unlike instruction-timing sources (LSB=0.015–0.026, always even), proc_info
//! timing is dominated by kernel lock scheduling — a higher-level stochastic
//! process with no microarchitectural quantization. The LSB distribution is
//! close to uniform (0.5), indicating the kernel path length has genuine
//! bit-level randomness.

use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};

#[cfg(target_os = "macos")]
use crate::sources::helpers::{extract_timing_entropy, mach_time};

static PROC_INFO_TIMING_INFO: SourceInfo = SourceInfo {
    name: "proc_info_timing",
    description: "proc_pidinfo / proc_pid_rusage syscall — kernel proc_lock contention timing",
    physics: "Times proc_pidinfo(TBSDINFO) and proc_pid_rusage(V4) syscalls. Each acquires \
              the BSD kernel proc_lock, walks the process table, and optionally reads hardware \
              perf counters. Lock contention from concurrent fork/exec/exit operations creates \
              variable delay. LSB=0.24\u{2013}0.29 (near-uniform, unlike always-even instruction \
              timing) — kernel scheduling dominates, not microarch quantization. CV=43\u{2013}48%, \
              range=[434,10583]. Cross-process sensitivity: any process lifecycle activity \
              (terminal commands, builds, browser tabs) leaks into our timing distribution \
              through proc_lock contention. Genuine covert channel.",
    category: SourceCategory::System,
    platform: Platform::MacOS,
    requirements: &[],
    entropy_rate_estimate: 1.5,
    composite: false,
    is_fast: false,
};

/// Entropy source from proc_info/proc_pid_rusage system call timing.
pub struct ProcInfoTimingSource;

#[cfg(target_os = "macos")]
unsafe extern "C" {
    // proc_pidinfo returns the number of bytes written, or -1 on error.
    fn proc_pidinfo(
        pid: i32,
        flavor: i32,
        arg: u64,
        buffer: *mut core::ffi::c_void,
        buffersize: i32,
    ) -> i32;

    // proc_pid_rusage fills in a rusage_info struct.
    // rusage_info_t is typedef void*, so the C sig is (int, int, rusage_info_t*) = (int, int, void**).
    // In practice the kernel writes directly into the pointed-to struct.
    fn proc_pid_rusage(pid: i32, flavor: i32, buffer: *mut core::ffi::c_void) -> i32;

    fn getpid() -> i32;
}

/// PROC_PIDTBSDINFO flavor — basic BSD process info.
#[cfg(target_os = "macos")]
const PROC_PIDTBSDINFO: i32 = 3;

/// RUSAGE_INFO_V4 — includes CPU cycles and memory bandwidth.
#[cfg(target_os = "macos")]
const RUSAGE_INFO_V4: i32 = 4;

#[cfg(target_os = "macos")]
#[repr(C, align(8))]
struct ProcBSDInfo {
    _pad: [u8; 512], // large enough for proc_bsdinfo
}

#[cfg(target_os = "macos")]
#[repr(C, align(8))]
struct RusageInfoV4 {
    _pad: [u8; 320], // rusage_info_v4 is 296 bytes; pad to 320 for safety
}

#[cfg(target_os = "macos")]
impl EntropySource for ProcInfoTimingSource {
    fn info(&self) -> &SourceInfo {
        &PROC_INFO_TIMING_INFO
    }

    fn is_available(&self) -> bool {
        true
    }

    fn collect(&self, n_samples: usize) -> Vec<u8> {
        let raw = n_samples * 2 + 32;
        let mut timings = Vec::with_capacity(raw * 2);

        let pid = unsafe { getpid() };
        let mut bsd_info = ProcBSDInfo { _pad: [0u8; 512] };
        let mut ru_info = RusageInfoV4 { _pad: [0u8; 320] };

        // Warm up — first call has extra kernel setup cost
        for _ in 0..4 {
            unsafe {
                proc_pidinfo(
                    pid,
                    PROC_PIDTBSDINFO,
                    0,
                    bsd_info._pad.as_mut_ptr() as *mut core::ffi::c_void,
                    bsd_info._pad.len() as i32,
                );
            }
        }

        for _ in 0..raw {
            // proc_pidinfo: process table + BSD info lock
            let t0 = mach_time();
            unsafe {
                proc_pidinfo(
                    pid,
                    PROC_PIDTBSDINFO,
                    0,
                    bsd_info._pad.as_mut_ptr() as *mut core::ffi::c_void,
                    bsd_info._pad.len() as i32,
                );
            }
            let t_pid = mach_time().wrapping_sub(t0);

            // proc_pid_rusage V4: performance counter cross-core read
            let t1 = mach_time();
            unsafe {
                proc_pid_rusage(
                    pid,
                    RUSAGE_INFO_V4,
                    ru_info._pad.as_mut_ptr() as *mut core::ffi::c_void,
                );
            }
            let t_ru = mach_time().wrapping_sub(t1);

            // Cap at 5ms (abnormal; suspend/resume artefact)
            if t_pid < 120_000 {
                timings.push(t_pid);
            }
            if t_ru < 120_000 {
                timings.push(t_ru);
            }
        }

        extract_timing_entropy(&timings, n_samples)
    }
}

#[cfg(not(target_os = "macos"))]
impl EntropySource for ProcInfoTimingSource {
    fn info(&self) -> &SourceInfo {
        &PROC_INFO_TIMING_INFO
    }
    fn is_available(&self) -> bool {
        false
    }
    fn collect(&self, _: usize) -> Vec<u8> {
        Vec::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn info() {
        let src = ProcInfoTimingSource;
        assert_eq!(src.info().name, "proc_info_timing");
        assert!(matches!(src.info().category, SourceCategory::System));
        assert_eq!(src.info().platform, Platform::MacOS);
        assert!(!src.info().composite);
    }

    #[test]
    #[cfg(target_os = "macos")]
    fn is_available_on_macos() {
        assert!(ProcInfoTimingSource.is_available());
    }

    #[test]
    #[ignore]
    fn collects_lock_contention_timing() {
        let data = ProcInfoTimingSource.collect(32);
        assert!(!data.is_empty());
    }
}