openentropy-core 0.12.3

Core entropy harvesting library — hardware noise sources, raw or SHA-256 conditioned
Documentation
//! Apple GXF (Guarded eXecution environment) register EL0 timing entropy.
//!
//! Apple Silicon introduces **GXF** (Guarded eXecution environment), Apple's
//! proprietary EL2-equivalent security layer that protects the hypervisor and
//! kernel memory regions. GXF registers occupy the `S3_6_c15_*` system-register
//! namespace, which is architecturally reserved and should be inaccessible at EL0.
//!
//! Systematic JIT probing of the `S3_6_c15_*` space reveals that register
//! **`S3_6_c15_c1_5`** (op1=6, CRn=c15, CRm=c1, op2=5) is **readable from EL0**,
//! returning a non-zero, non-timer value:
//!
//! ```text
//! S3_6_c15_c1_5 = 0x2010002030100000  (constant — capability/permission bitmask)
//! ```
//!
//! While the register's value is static, its **read latency** shows useful entropy:
//!
//! ```text
//! Timing histogram (N=500, Mac mini M4):
//!   t= 0 ticks:  26 samples ( 5%) — fast path (pipeline optimisation)
//!   t=41 ticks: 134 samples (27%) — single trap-and-emulate cycle
//!   t=42 ticks: 300 samples (60%) — trap + 1 extra cycle (pipeline hazard)
//!   t=83 ticks:  27 samples ( 5%) — double trap cycle (GXF state busy)
//!   t=84 ticks:  13 samples ( 3%) — double trap + hazard
//!   CV=35.2%, LSB P(odd)=0.322
//! ```
//!
//! ## Physics
//!
//! The multi-modal timing distribution reflects the GXF trap-and-emulate mechanism:
//!
//! 1. **t≈0 (5%)** — Occasionally the read is served from the ARM architectural
//!    system-register pipeline shortcut before the GXF intercept activates.
//!
//! 2. **t≈41 (27%)** — Single GXF trap cycle: the kernel intercepts the MRS
//!    instruction, consults the GXF register permissions table, and returns the
//!    permitted value. 41 ticks ≈ 1.71 µs at 24 MHz, consistent with a minimal
//!    kernel entry/exit round-trip on Apple Silicon (cf. APRR toggle at 42 ticks).
//!
//! 3. **t≈42 (60%)** — Trap + 1 pipeline-hazard cycle. The most common path:
//!    the trap completes but an instruction-fetch hazard adds 1 tick on return.
//!
//! 4. **t≈83 (5%)** — Double trap cycle. GXF state is transiently busy
//!    (contested between core and the GXF security monitor), requiring a retry.
//!    83 ≈ 2×41+1, consistent with serialised double-trap latency.
//!
//! ## Security significance
//!
//! The fact that this GXF namespace register is readable from EL0 is a **novel
//! finding** from systematic JIT probing of Apple Silicon registers (2026).
//! GXF registers in the `S3_6_c15_*` namespace should require EL1 or GXF-level
//! privilege. The accessible register likely exposes a read-only capability or
//! permission configuration. The **timing behaviour** of the trap path encodes
//! the GXF monitor's internal scheduling state.
//!
//! ## Prior art
//!
//! - Sven Peter, "SPRR and GXF", 2021: documents GXF entry vector registers
//!   (`S3_6_c15_c8_0`, etc.) at EL1/EL2 only; does not survey EL0-accessible GXF
//!   registers or characterise their timing as an entropy source.
//!   <https://blog.svenpeter.dev/posts/m1_sprr_gxf/>
//! - siguza, "APRR", 2020: surveys Apple private registers; GXF timing not studied.
//!   <https://siguza.github.io/APRR/>

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

static GXF_REGISTER_TIMING_INFO: SourceInfo = SourceInfo {
    name: "gxf_register_timing",
    description: "Apple GXF EL0-accessible register trap-path timing entropy",
    physics: "S3_6_c15_c1_5 (GXF namespace) is readable from EL0 via JIT-generated MRS, \
              producing a multimodal timing distribution: 0/41/42/83/84 ticks, CV=35.2%. \
              Modes reflect the GXF trap-and-emulate path: 0=pipeline shortcut, \
              41=single trap cycle, 42=trap+hazard, 83=double trap (GXF monitor busy). \
              41-tick trap latency matches APRR toggle latency, confirming Apple EL1 \
              security monitor round-trip time. Entropy encodes GXF monitor scheduling \
              state. Register value is static (0x2010002030100000, capability bitmask); \
              entropy comes solely from trap-path timing variation. Novel finding: first \
              EL0-accessible GXF namespace register characterised as entropy source.",
    category: SourceCategory::Microarch,
    platform: Platform::MacOS,
    requirements: &[Requirement::AppleSilicon],
    entropy_rate_estimate: 0.7,
    composite: false,
    is_fast: false,
};

/// Entropy from Apple GXF security monitor trap-path read latency.
pub struct GxfRegisterTimingSource;

#[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::Once;
    use std::sync::atomic::{AtomicBool, Ordering};

    // S3_6_c15_c1_5: op0=3, op1=6, CRn=c15, CRm=c1, op2=5
    // 0xD5380000 | (6<<16)|(15<<12)|(1<<8)|(5<<5)|0
    const GXF_MRS_X0: u32 = 0xD5380000u32
        | (6u32 << 16)   // op1=6
        | (15u32 << 12)  // CRn=c15
        | (1u32 << 8)    // CRm=c1
        | (5u32 << 5); // op2=5, Rt=X0
    const RET: u32 = 0xD65F03C0u32;

    type FnPtr = unsafe extern "C" fn() -> u64;

    /// RAII guard for a JIT mmap page — ensures munmap on drop (including panic unwind).
    struct JitPage(*mut libc::c_void);

    impl Drop for JitPage {
        fn drop(&mut self) {
            unsafe {
                libc::munmap(self.0, 4096);
            }
        }
    }

    static CHECKED: Once = Once::new();
    static AVAILABLE: AtomicBool = AtomicBool::new(false);

    /// Build a JIT page with MRS S3_6_c15_c1_5 + RET.
    /// Returns (fn_ptr, page_guard) or None on failure.
    /// The JitPage guard ensures munmap on drop.
    unsafe fn build_jit() -> Option<(FnPtr, JitPage)> {
        let page = unsafe {
            libc::mmap(
                std::ptr::null_mut(),
                4096,
                libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | 0x0800, // MAP_JIT = 0x0800
                -1,
                0,
            )
        };
        if page == libc::MAP_FAILED {
            return None;
        }
        unsafe {
            libc::pthread_jit_write_protect_np(0);
            let code = page as *mut u32;
            code.write(GXF_MRS_X0);
            code.add(1).write(RET);
            libc::pthread_jit_write_protect_np(1);
            core::arch::asm!("dc cvau, {p}", "ic ivau, {p}", p = in(reg) page, options(nostack));
            core::arch::asm!("dsb ish", "isb", options(nostack));
        }
        let fn_ptr: FnPtr = unsafe { std::mem::transmute(page) };
        Some((fn_ptr, JitPage(page)))
    }

    #[inline]
    unsafe fn time_gxf(fn_ptr: FnPtr) -> u64 {
        core::sync::atomic::fence(Ordering::SeqCst);
        let t0 = mach_time();
        let _v = unsafe { fn_ptr() };
        let t1 = mach_time();
        core::sync::atomic::fence(Ordering::SeqCst);
        t1.wrapping_sub(t0)
    }

    impl EntropySource for GxfRegisterTimingSource {
        fn info(&self) -> &SourceInfo {
            &GXF_REGISTER_TIMING_INFO
        }

        fn is_available(&self) -> bool {
            // S3_6_c15_c1_5 may not be accessible on all Apple Silicon chips/OS versions.
            // Use a fork-based probe to safely test instruction execution without
            // risking SIGILL in the main process.
            CHECKED.call_once(|| {
                let ok = crate::sources::helpers::probe_jit_instruction_safe(GXF_MRS_X0);
                AVAILABLE.store(ok, Ordering::SeqCst);
            });
            AVAILABLE.load(Ordering::SeqCst)
        }

        fn collect(&self, n_samples: usize) -> Vec<u8> {
            unsafe {
                let Some((fn_ptr, _page_guard)) = build_jit() else {
                    return Vec::new();
                };

                // Warmup
                for _ in 0..32 {
                    let _ = time_gxf(fn_ptr);
                }

                let raw_count = n_samples * 8 + 256;
                let mut timings = Vec::with_capacity(raw_count);

                for _ in 0..raw_count {
                    let t = time_gxf(fn_ptr);
                    // Accept values in [0, 200]; reject interrupt-induced outliers
                    if t <= 200 {
                        timings.push(t);
                    }
                }

                // _page_guard drops here, calling munmap automatically
                extract_timing_entropy_debiased(&timings, n_samples)
            }
        }
    }
}

#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
impl EntropySource for GxfRegisterTimingSource {
    fn info(&self) -> &SourceInfo {
        &GXF_REGISTER_TIMING_INFO
    }
    fn is_available(&self) -> bool {
        false
    }
    fn collect(&self, _n_samples: usize) -> Vec<u8> {
        Vec::new()
    }
}

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

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

    #[test]
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    fn availability_probe_does_not_crash() {
        let src = GxfRegisterTimingSource;
        // The register may or may not be accessible depending on chip/OS version.
        // Just verify the probe completes without crashing (no SIGILL).
        let _ = src.is_available();
    }

    #[test]
    #[ignore] // Requires EL0-accessible GXF register (verified on M4 Mac mini)
    fn collects_multimodal_timing() {
        let src = GxfRegisterTimingSource;
        if !src.is_available() {
            return;
        }
        let data = src.collect(32);
        assert!(!data.is_empty());
        let unique: std::collections::HashSet<u8> = data.iter().copied().collect();
        assert!(unique.len() >= 2, "expected GXF trap-path timing variation");
    }
}