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