openentropy_core/sources/
disk.rs1use std::io::{Read, Seek, SeekFrom, Write};
10use std::time::Instant;
11
12use tempfile::NamedTempFile;
13
14use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
15
16const TEMP_FILE_SIZE: usize = 64 * 1024; const READ_BLOCK_SIZE: usize = 4 * 1024; static DISK_IO_INFO: SourceInfo = SourceInfo {
23 name: "disk_io",
24 description: "NVMe/SSD read latency jitter from random 4KB reads",
25 physics: "Measures NVMe/SSD read latency for small random reads. Jitter sources: \
26 flash translation layer (FTL) remapping, wear leveling, garbage collection, \
27 read disturb mitigation, NAND page read latency variation (depends on charge \
28 level in floating-gate transistors), and NVMe controller queue arbitration.",
29 category: SourceCategory::IO,
30 platform: Platform::Any,
31 requirements: &[],
32 entropy_rate_estimate: 800.0,
33 composite: false,
34};
35
36pub struct DiskIOSource;
38
39impl EntropySource for DiskIOSource {
40 fn info(&self) -> &SourceInfo {
41 &DISK_IO_INFO
42 }
43
44 fn is_available(&self) -> bool {
45 true
46 }
47
48 fn collect(&self, n_samples: usize) -> Vec<u8> {
49 let mut tmpfile = match NamedTempFile::new() {
51 Ok(f) => f,
52 Err(_) => return Vec::new(),
53 };
54
55 let mut fill_data = vec![0u8; TEMP_FILE_SIZE];
56 let mut lcg: u64 = 0xCAFE_BABE_DEAD_BEEF;
57 for chunk in fill_data.chunks_mut(8) {
58 lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
59 let bytes = lcg.to_le_bytes();
60 for (i, b) in chunk.iter_mut().enumerate() {
61 *b = bytes[i % 8];
62 }
63 }
64 if tmpfile.write_all(&fill_data).is_err() {
65 return Vec::new();
66 }
67 if tmpfile.flush().is_err() {
68 return Vec::new();
69 }
70
71 let mut raw = Vec::with_capacity(n_samples);
72 let mut read_buf = vec![0u8; READ_BLOCK_SIZE];
73 let max_offset = TEMP_FILE_SIZE.saturating_sub(READ_BLOCK_SIZE);
74
75 let seed = std::time::SystemTime::now()
76 .duration_since(std::time::UNIX_EPOCH)
77 .unwrap_or_default()
78 .as_nanos() as u64;
79 let mut lcg_state = seed | 1;
80
81 let mut prev_ns: u64 = 0;
82
83 let num_reads = n_samples * 2 + 64;
85
86 for i in 0..num_reads {
87 lcg_state = lcg_state
88 .wrapping_mul(6364136223846793005)
89 .wrapping_add(1442695040888963407);
90 let offset = (lcg_state as usize) % (max_offset + 1);
91
92 let t0 = Instant::now();
93 let _ = tmpfile.seek(SeekFrom::Start(offset as u64));
94 let _ = tmpfile.read(&mut read_buf);
95 let elapsed_ns = t0.elapsed().as_nanos() as u64;
96
97 if i > 0 {
98 let delta = elapsed_ns.wrapping_sub(prev_ns);
99 raw.push(delta as u8);
101 }
102
103 prev_ns = elapsed_ns;
104
105 if raw.len() >= n_samples {
106 break;
107 }
108 }
109
110 raw.truncate(n_samples);
111 raw
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 #[ignore] fn disk_io_collects_bytes() {
122 let src = DiskIOSource;
123 assert!(src.is_available());
124 let data = src.collect(128);
125 assert!(!data.is_empty());
126 }
127
128 #[test]
129 fn disk_io_info() {
130 let src = DiskIOSource;
131 assert_eq!(src.name(), "disk_io");
132 assert_eq!(src.info().category, SourceCategory::IO);
133 }
134}