Skip to main content

openentropy_core/sources/frontier/
nvme_latency.rs

1//! NVMe I/O stack timing — storage controller scheduling entropy.
2//!
3//! Each read with F_NOCACHE (bypassing OS buffer cache) traverses:
4//! - Filesystem metadata lookup and block mapping
5//! - NVMe command submission and completion queue arbitration
6//! - SSD controller DRAM cache lookup and scheduling
7//! - SSD internal firmware scheduling (garbage collection, wear leveling)
8//!
9//! Note: the file is freshly written, so data typically resides in the SSD's
10//! internal DRAM cache rather than NAND cells. The entropy comes from I/O
11//! stack scheduling nondeterminism, not NAND cell physics.
12//!
13
14use std::io::{Read, Seek, SeekFrom, Write};
15use std::time::Instant;
16
17use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
18use crate::sources::helpers::extract_timing_entropy;
19
20/// Number of distinct offsets to cycle through (hitting different NAND pages).
21const N_OFFSETS: usize = 8;
22
23/// Block size for each read.
24const BLOCK_SIZE: usize = 4096;
25
26static NVME_LATENCY_INFO: SourceInfo = SourceInfo {
27    name: "nvme_latency",
28    description: "NVMe I/O stack timing jitter from storage controller scheduling",
29    physics: "Reads a file at multiple offsets with OS buffer cache bypassed (F_NOCACHE). \
30              Each read traverses: filesystem metadata lookup \u{2192} NVMe command queue \
31              submission \u{2192} SSD controller DRAM cache \u{2192} completion interrupt. \
32              Timing jitter arises from NVMe command queue arbitration, SSD controller \
33              firmware scheduling (garbage collection, wear leveling background tasks), \
34              and interrupt delivery latency. Note: freshly-written data typically resides \
35              in SSD DRAM cache, not NAND cells.",
36    category: SourceCategory::IO,
37    platform: Platform::Any,
38    requirements: &[],
39    entropy_rate_estimate: 1000.0,
40    composite: false,
41};
42
43/// Entropy source that harvests timing jitter from NVMe flash cell reads.
44pub struct NVMeLatencySource;
45
46impl EntropySource for NVMeLatencySource {
47    fn info(&self) -> &SourceInfo {
48        &NVME_LATENCY_INFO
49    }
50
51    fn is_available(&self) -> bool {
52        true
53    }
54
55    fn collect(&self, n_samples: usize) -> Vec<u8> {
56        // Create a temp file with varied data across multiple offsets.
57        let mut tmpfile = match tempfile::NamedTempFile::new() {
58            Ok(f) => f,
59            Err(_) => return Vec::new(),
60        };
61
62        let total_size = BLOCK_SIZE * N_OFFSETS;
63        let mut fill = vec![0u8; total_size];
64        let mut lcg: u64 = 0xDEAD_BEEF_CAFE_1234;
65        for chunk in fill.chunks_mut(8) {
66            lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
67            let bytes = lcg.to_le_bytes();
68            for (i, b) in chunk.iter_mut().enumerate() {
69                *b = bytes[i % 8];
70            }
71        }
72        if tmpfile.write_all(&fill).is_err() {
73            return Vec::new();
74        }
75        if tmpfile.flush().is_err() {
76            return Vec::new();
77        }
78
79        // Disable buffer caching on macOS.
80        #[cfg(target_os = "macos")]
81        {
82            use std::os::unix::io::AsRawFd;
83            // SAFETY: F_NOCACHE is a valid fcntl command on macOS that disables
84            // the unified buffer cache for this file descriptor.
85            unsafe {
86                libc::fcntl(tmpfile.as_raw_fd(), libc::F_NOCACHE, 1);
87            }
88        }
89
90        let raw_count = n_samples * 4 + 64;
91        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
92        let mut read_buf = vec![0u8; BLOCK_SIZE];
93
94        for i in 0..raw_count {
95            let offset = (i % N_OFFSETS) as u64 * BLOCK_SIZE as u64;
96            if tmpfile.seek(SeekFrom::Start(offset)).is_err() {
97                continue;
98            }
99            let t0 = Instant::now();
100            let _ = tmpfile.read(&mut read_buf);
101            let elapsed = t0.elapsed();
102            timings.push(elapsed.as_nanos() as u64);
103        }
104
105        extract_timing_entropy(&timings, n_samples)
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn info() {
115        let src = NVMeLatencySource;
116        assert_eq!(src.name(), "nvme_latency");
117        assert_eq!(src.info().category, SourceCategory::IO);
118        assert!(!src.info().composite);
119    }
120
121    #[test]
122    #[ignore] // I/O dependent
123    fn collects_bytes() {
124        let src = NVMeLatencySource;
125        assert!(src.is_available());
126        let data = src.collect(64);
127        assert!(!data.is_empty());
128        assert!(data.len() <= 64);
129    }
130}