openentropy_core/sources/io/
nvme_passthrough_linux.rs1use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
27#[cfg(target_os = "linux")]
28use crate::sources::helpers::extract_timing_entropy;
29
30static NVME_PASSTHROUGH_INFO: SourceInfo = SourceInfo {
31 name: "nvme_passthrough_linux",
32 description: "Raw NVMe admin commands via ioctl passthrough on Linux (closest to NAND hardware)",
33 physics: "Submits NVMe admin commands (Get Log Page for SMART/Health Information, Log ID 02h) \
34 via ioctl(NVME_IOCTL_ADMIN_CMD) on /dev/nvme0. This bypasses the filesystem, block \
35 layer, and I/O scheduler entirely. The timing path is: userspace \u{2192} NVMe kernel \
36 driver \u{2192} NVMe controller \u{2192} NAND flash. Command round-trip timing is \
37 dominated by NVMe controller firmware processing (FTL lookup, wear leveling, garbage \
38 collection scheduling) and NAND flash page access. NAND charge sensing has quantum-\
39 mechanical underpinnings (Fowler-Nordheim tunneling), but the dominant timing variance \
40 is classical (driver overhead, firmware scheduling). SMART temperature values provide \
41 additional ADC quantization noise.",
42 category: SourceCategory::IO,
43 platform: Platform::Linux,
44 requirements: &[Requirement::RawBlockDevice],
45 entropy_rate_estimate: 2.0,
46 composite: false,
47 is_fast: true,
48};
49
50pub struct NvmePassthroughLinuxSource;
52
53#[cfg(target_os = "linux")]
55mod passthrough {
56 use std::time::Instant;
57
58 #[repr(C)]
61 #[derive(Default)]
62 struct NvmePassthruCmd {
63 opcode: u8,
64 flags: u8,
65 rsvd1: u16,
66 nsid: u32,
67 cdw2: u32,
68 cdw3: u32,
69 metadata: u64,
70 addr: u64,
71 metadata_len: u32,
72 data_len: u32,
73 cdw10: u32,
74 cdw11: u32,
75 cdw12: u32,
76 cdw13: u32,
77 cdw14: u32,
78 cdw15: u32,
79 timeout_ms: u32,
80 result: u32,
81 }
82
83 const _: () = assert!(std::mem::size_of::<NvmePassthruCmd>() == 72);
85
86 const NVME_IOCTL_ADMIN_CMD: libc::c_ulong = 0xC048_4E41;
91
92 const NVME_ADMIN_GET_LOG_PAGE: u8 = 0x02;
94 const NVME_LOG_SMART: u32 = 0x02;
96 const SMART_LOG_SIZE: u32 = 512;
98
99 pub fn try_open_nvme() -> Option<i32> {
101 let devices = ["/dev/nvme0", "/dev/nvme1", "/dev/nvme0n1"];
102 for dev in &devices {
103 let c_path = match std::ffi::CString::new(*dev) {
104 Ok(s) => s,
105 Err(_) => continue,
106 };
107 let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDONLY) };
110 if fd >= 0 {
111 return Some(fd);
112 }
113 }
114 None
115 }
116
117 pub fn has_nvme_passthrough() -> bool {
119 if let Some(fd) = try_open_nvme() {
120 unsafe { libc::close(fd) };
122 true
123 } else {
124 false
125 }
126 }
127
128 fn submit_smart_log_page(fd: i32) -> Option<(u64, u16)> {
131 let mut log_buf = [0u8; SMART_LOG_SIZE as usize];
132
133 let numd = (SMART_LOG_SIZE / 4) - 1;
135
136 let mut cmd = NvmePassthruCmd {
137 opcode: NVME_ADMIN_GET_LOG_PAGE,
138 nsid: 0xFFFF_FFFF, addr: log_buf.as_mut_ptr() as u64,
140 data_len: SMART_LOG_SIZE,
141 cdw10: (numd << 16) | NVME_LOG_SMART, timeout_ms: 1000,
143 ..Default::default()
144 };
145
146 let t_before = Instant::now();
147
148 let ret =
152 unsafe { libc::ioctl(fd, NVME_IOCTL_ADMIN_CMD, &mut cmd as *mut NvmePassthruCmd) };
153
154 let elapsed_nanos = t_before.elapsed().as_nanos() as u64;
155
156 if ret < 0 {
157 return None;
158 }
159
160 let temp_kelvin = u16::from_le_bytes([log_buf[1], log_buf[2]]);
162
163 Some((elapsed_nanos, temp_kelvin))
164 }
165
166 pub fn timed_smart_reads(fd: i32, count: usize) -> (Vec<u64>, Vec<u16>) {
168 let mut timings = Vec::with_capacity(count);
169 let mut temps = Vec::with_capacity(count);
170
171 for _ in 0..count {
172 match submit_smart_log_page(fd) {
173 Some((timing, temp)) => {
174 timings.push(timing);
175 temps.push(temp);
176 }
177 None => {
178 }
180 }
181 }
182
183 (timings, temps)
184 }
185}
186
187impl EntropySource for NvmePassthroughLinuxSource {
188 fn info(&self) -> &SourceInfo {
189 &NVME_PASSTHROUGH_INFO
190 }
191
192 fn is_available(&self) -> bool {
193 #[cfg(target_os = "linux")]
194 {
195 passthrough::has_nvme_passthrough()
196 }
197 #[cfg(not(target_os = "linux"))]
198 {
199 false
200 }
201 }
202
203 fn collect(&self, n_samples: usize) -> Vec<u8> {
204 #[cfg(not(target_os = "linux"))]
205 {
206 let _ = n_samples;
207 Vec::new()
208 }
209
210 #[cfg(target_os = "linux")]
211 {
212 use crate::sources::helpers::xor_fold_u64;
213
214 let fd = match passthrough::try_open_nvme() {
215 Some(fd) => fd,
216 None => return Vec::new(),
217 };
218
219 let raw_count = n_samples * 4 + 64;
221 let (timings, temps) = passthrough::timed_smart_reads(fd, raw_count);
222
223 unsafe { libc::close(fd) };
225
226 if timings.len() < 4 {
227 return Vec::new();
228 }
229
230 let timing_bytes = extract_timing_entropy(&timings, n_samples);
232
233 let temp_deltas: Vec<u64> = temps
235 .windows(2)
236 .map(|w| (w[1] as u64).wrapping_sub(w[0] as u64))
237 .collect();
238 let temp_xored: Vec<u64> = temp_deltas.windows(2).map(|w| w[0] ^ w[1]).collect();
239 let temp_bytes: Vec<u8> = temp_xored
240 .iter()
241 .map(|&x| xor_fold_u64(x))
242 .take(n_samples)
243 .collect();
244
245 let mut output = Vec::with_capacity(n_samples);
247 for i in 0..timing_bytes.len().max(temp_bytes.len()).min(n_samples) {
248 let tb = timing_bytes.get(i).copied().unwrap_or(0);
249 let tempb = temp_bytes.get(i).copied().unwrap_or(0);
250 output.push(tb ^ tempb);
251 }
252 output.truncate(n_samples);
253 output
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn info() {
264 let src = NvmePassthroughLinuxSource;
265 assert_eq!(src.name(), "nvme_passthrough_linux");
266 assert_eq!(src.info().category, SourceCategory::IO);
267 assert_eq!(src.info().platform, Platform::Linux);
268 assert!(!src.info().composite);
269 }
270
271 #[test]
272 fn physics_mentions_ioctl() {
273 let src = NvmePassthroughLinuxSource;
274 assert!(src.info().physics.contains("ioctl"));
275 assert!(src.info().physics.contains("SMART"));
276 assert!(src.info().physics.contains("Fowler-Nordheim"));
277 }
278
279 #[test]
280 fn not_available_on_non_linux() {
281 let src = NvmePassthroughLinuxSource;
282 #[cfg(not(target_os = "linux"))]
283 assert!(!src.is_available());
284 #[cfg(target_os = "linux")]
285 let _ = src; }
287
288 #[test]
289 #[ignore] fn collects_bytes() {
291 let src = NvmePassthroughLinuxSource;
292 if src.is_available() {
293 let data = src.collect(64);
294 assert!(!data.is_empty());
295 assert!(data.len() <= 64);
296 }
297 }
298}