openentropy-core 0.12.3

Core entropy harvesting library — hardware noise sources, raw or SHA-256 conditioned
Documentation
//! SITVA — Scheduler-Induced Timing Variance Amplification.
//!
//! ## Discovery
//!
//! During deep hardware probing of Apple M4 (2026-02-24), a companion
//! thread running continuous NEON FMLA instructions was found to **triple**
//! the timing variance of AES measurements on another thread:
//!
//! ```text
//!                  Baseline   Under FMLA load   Δ
//! ISB+CNTVCT CV:   30.3%      113.3%            +83 pp
//! AES 2-round CV:  66.4%      189.4%            +123 pp
//! ```
//!
//! ## Mechanism
//!
//! When the companion thread creates sustained compute load, the macOS
//! scheduler responds by:
//!
//! 1. Promoting threads to P-cores (higher clock, different pipeline timing)
//! 2. Increasing preemption frequency to service the load thread
//! 3. Creating two distinct execution paths for the measurement thread:
//!    - **Fast path** (post-preemption): L1 refilled, pipeline freshly primed
//!    - **Slow path** (steady state): normal execution on shared execution units
//!
//! The stochastic boundary between fast/slow encodes:
//! - OS scheduler quantum timing (microsecond resolution)  
//! - P-core vs E-core migration decision history
//! - Thermal state and DVFS decisions
//! - Preemption depth at time of measurement
//!
//! ## Why This Is Novel
//!
//! All prior entropy libraries measure timing in **isolation** — companion
//! threads are treated as noise to be eliminated, not as amplifiers.
//! SITVA deliberately creates controlled interference: the companion
//! thread is the *entropy mechanism*, not a background artefact.
//!
//! The closest prior work is jitterentropy (Müller 2017), which uses
//! memory access timing jitter. SITVA differs in that the variance is
//! *induced by a controlled external load* rather than harvested from
//! passive hardware noise. No entropy library characterised the
//! amplification effect before this work (2026).
//!
//! ## Characterisation (Mac mini M4, macOS 15.3)
//!
//! ```text
//! AES CV baseline:     66.4%
//! AES CV under FMLA:  189.4%   (2.85× amplification)
//! Amplification onset: ~100ms after companion start
//! Amplification decay: ~100ms after companion stops
//! Distribution:        bimodal — fast (0–17t) / slow (41–59t)
//! ```

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

static SITVA_INFO: SourceInfo = SourceInfo {
    name: "sitva",
    description: "Scheduler-induced timing variance amplification via NEON FMLA companion thread",
    physics: "Spawns a companion thread running continuous NEON FMLA (FP multiply-accumulate) \
              bursts. The macOS scheduler responds by increasing preemption frequency and \
              migrating threads across P/E cores, which creates a bimodal AES timing \
              distribution: fast (post-preemption L1-refill burst, 0–17 ticks) vs slow \
              (steady-state P-core, 41–59 ticks). AES CV triples: 66% baseline → 189% \
              under load. The stochastic preemption boundary encodes OS scheduler quantum \
              timing, P/E-core migration decisions, thermal state, and DVFS history. \
              Novel primitive: no prior entropy library deliberately uses a companion \
              computation thread as a variance amplifier (discovered 2026-02-24).",
    category: SourceCategory::Microarch,
    platform: Platform::MacOS,
    requirements: &[Requirement::AppleSilicon],
    entropy_rate_estimate: 2.0, // CV=189% × AES sample rate
    composite: false,
    is_fast: false,
};

/// Entropy from scheduler preemption patterns amplified by a NEON FMLA companion thread.
pub struct SITVASource;

#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
mod imp {
    use super::*;
    use crate::sources::helpers::extract_timing_entropy_debiased;
    use crate::sources::helpers::mach_time;
    use std::sync::Arc;
    use std::sync::atomic::{AtomicBool, Ordering};

    // Companion thread: runs NEON FMLA in 32-instruction bursts, yields between.
    // The yield prevents starvation while keeping scheduler pressure high.
    fn companion_body(stop: Arc<AtomicBool>) {
        unsafe {
            // Initialise v0-v7 with non-zero values
            core::arch::asm!(
                "fmov v0.4s, #1.0",
                "fmov v1.4s, #1.5",
                "fmov v2.4s, #2.0",
                "fmov v3.4s, #2.5",
                "fmov v4.4s, #0.5",
                "fmov v5.4s, #1.25",
                "fmov v6.4s, #0.75",
                "fmov v7.4s, #1.75",
                out("v0") _, out("v1") _, out("v2") _, out("v3") _,
                out("v4") _, out("v5") _, out("v6") _, out("v7") _,
                options(nostack),
            );
        }

        while !stop.load(Ordering::Relaxed) {
            // 32× FMLA — fills the FP execution unit, maximises scheduler pressure.
            // Note: `out` (not `inout`) is intentional. The goal is FP unit saturation
            // for scheduler pressure, not mathematically correct FMLA accumulation.
            // Register values between iterations are irrelevant — even if the compiler
            // clobbers v0-v7 between asm blocks (unlikely: only a bool load separates
            // them), the FMLA burst still occupies the execution units for ~32 cycles.
            unsafe {
                core::arch::asm!(
                    "fmla v0.4s, v1.4s, v2.4s",
                    "fmla v1.4s, v2.4s, v3.4s",
                    "fmla v2.4s, v3.4s, v4.4s",
                    "fmla v3.4s, v4.4s, v5.4s",
                    "fmla v4.4s, v5.4s, v6.4s",
                    "fmla v5.4s, v6.4s, v7.4s",
                    "fmla v6.4s, v7.4s, v0.4s",
                    "fmla v7.4s, v0.4s, v1.4s",
                    "fmla v0.4s, v1.4s, v2.4s",
                    "fmla v1.4s, v2.4s, v3.4s",
                    "fmla v2.4s, v3.4s, v4.4s",
                    "fmla v3.4s, v4.4s, v5.4s",
                    "fmla v4.4s, v5.4s, v6.4s",
                    "fmla v5.4s, v6.4s, v7.4s",
                    "fmla v6.4s, v7.4s, v0.4s",
                    "fmla v7.4s, v0.4s, v1.4s",
                    "fmla v0.4s, v1.4s, v2.4s",
                    "fmla v1.4s, v2.4s, v3.4s",
                    "fmla v2.4s, v3.4s, v4.4s",
                    "fmla v3.4s, v4.4s, v5.4s",
                    "fmla v4.4s, v5.4s, v6.4s",
                    "fmla v5.4s, v6.4s, v7.4s",
                    "fmla v6.4s, v7.4s, v0.4s",
                    "fmla v7.4s, v0.4s, v1.4s",
                    "fmla v0.4s, v1.4s, v2.4s",
                    "fmla v1.4s, v2.4s, v3.4s",
                    "fmla v2.4s, v3.4s, v4.4s",
                    "fmla v3.4s, v4.4s, v5.4s",
                    "fmla v4.4s, v5.4s, v6.4s",
                    "fmla v5.4s, v6.4s, v7.4s",
                    "fmla v6.4s, v7.4s, v0.4s",
                    "fmla v7.4s, v0.4s, v1.4s",
                    out("v0") _, out("v1") _, out("v2") _, out("v3") _,
                    out("v4") _, out("v5") _, out("v6") _, out("v7") _,
                    options(nostack),
                );
            }
            // Yield between bursts — prevents starvation of measurement thread
            std::thread::yield_now();
        }
    }

    /// Time 2 rounds of AES (AESE+AESMC × 2) under live scheduler pressure.
    #[inline]
    fn time_aes_under_load() -> u64 {
        let t0 = mach_time();
        unsafe {
            core::arch::asm!(
                // Load dummy key into v8, plaintext into v9
                "fmov v8.4s, #1.5",
                "fmov v9.4s, #2.5",
                // 2 AES rounds
                "aese v9.16b, v8.16b",
                "aesmc v9.16b, v9.16b",
                "aese v9.16b, v8.16b",
                "aesmc v9.16b, v9.16b",
                out("v8") _, out("v9") _,
                options(nostack),
            );
        }
        mach_time().wrapping_sub(t0)
    }

    impl EntropySource for SITVASource {
        fn info(&self) -> &SourceInfo {
            &SITVA_INFO
        }

        fn is_available(&self) -> bool {
            true // Always available on Apple Silicon with std threads
        }

        fn collect(&self, n_samples: usize) -> Vec<u8> {
            let stop = Arc::new(AtomicBool::new(false));
            let stop_clone = Arc::clone(&stop);

            // Spawn companion thread
            let handle = std::thread::spawn(move || companion_body(stop_clone));

            // Give the companion 50ms to spin up and trigger scheduler adaptation
            std::thread::sleep(std::time::Duration::from_millis(50));

            // Collect AES timing samples under amplified variance
            let raw_count = n_samples * 4 + 128;
            let mut timings = Vec::with_capacity(raw_count);

            for _ in 0..raw_count {
                timings.push(time_aes_under_load());
            }

            // Stop companion thread and wait for it to exit cleanly
            stop.store(true, Ordering::Relaxed);
            let _ = handle.join();

            extract_timing_entropy_debiased(&timings, n_samples)
        }
    }
}

#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
impl EntropySource for SITVASource {
    fn info(&self) -> &SourceInfo {
        &SITVA_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 = SITVASource;
        assert_eq!(src.info().name, "sitva");
        assert!(matches!(src.info().category, SourceCategory::Microarch));
        assert_eq!(src.info().platform, Platform::MacOS);
        assert!(!src.info().composite);
        assert!(src.info().entropy_rate_estimate > 1.0 && src.info().entropy_rate_estimate <= 8.0);
    }

    #[test]
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    fn available_on_apple_silicon() {
        assert!(SITVASource.is_available());
    }

    #[test]
    #[ignore] // spawns a live FMLA thread — excluded from fast CI
    fn amplified_variance_exceeds_baseline() {
        // Baseline: measure AES without companion thread
        let baseline_cv = {
            let mut t = Vec::new();
            for _ in 0..500 {
                let t0 = crate::sources::helpers::mach_time();
                unsafe {
                    core::arch::asm!(
                        "fmov v8.4s, #1.5", "fmov v9.4s, #2.5",
                        "aese v9.16b, v8.16b", "aesmc v9.16b, v9.16b",
                        out("v8") _, out("v9") _, options(nostack)
                    );
                }
                t.push(crate::sources::helpers::mach_time() - t0);
            }
            let mean: f64 = t.iter().map(|&x| x as f64).sum::<f64>() / 500.0;
            let var: f64 = t.iter().map(|&x| (x as f64 - mean).powi(2)).sum::<f64>() / 500.0;
            100.0 * var.sqrt() / mean
        };

        // SITVA: collect bytes (companion thread runs internally)
        let src = SITVASource;
        let data = src.collect(64);
        assert!(!data.is_empty());

        // We can't directly measure CV here, but we can verify output exists
        // and assert the source produces more distinct byte values than random chance
        let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
        assert!(
            unique.len() > 16,
            "expected high-entropy SITVA output (got {} unique bytes, baseline CV={:.1}%)",
            unique.len(),
            baseline_cv
        );
    }
}