openentropy_core/sources/io/
fsync_journal.rs1use std::io::Write;
18
19use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
20use crate::sources::helpers::extract_timing_entropy;
21
22static FSYNC_JOURNAL_INFO: SourceInfo = SourceInfo {
23 name: "fsync_journal",
24 description: "Filesystem journal commit timing from full storage stack traversal",
25 physics: "Creates a file, writes data, and calls fsync to force a full journal commit. \
26 Each commit traverses the entire storage stack: CPU \u{2192} filesystem \
27 (journal/CoW update, metadata allocation, checksum) \u{2192} storage controller \
28 (command queuing, arbitration) \u{2192} storage media (NAND cell programming or \
29 magnetic head seek). Every layer contributes independent timing noise from \
30 physically distinct sources. On macOS this exercises APFS; on Linux, ext4/XFS.",
31 category: SourceCategory::IO,
32 platform: Platform::Any,
33 requirements: &[],
34 entropy_rate_estimate: 2.0,
35 composite: false,
36 is_fast: false,
37};
38
39pub struct FsyncJournalSource;
41
42impl EntropySource for FsyncJournalSource {
43 fn info(&self) -> &SourceInfo {
44 &FSYNC_JOURNAL_INFO
45 }
46
47 fn is_available(&self) -> bool {
48 true
49 }
50
51 fn collect(&self, n_samples: usize) -> Vec<u8> {
52 let raw_count = n_samples * 2 + 64;
53 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
54 let write_data = [0xAAu8; 512];
55 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(4);
56
57 for i in 0..raw_count {
58 if i % 64 == 0 && std::time::Instant::now() >= deadline {
59 break;
60 }
61 let mut tmpfile = match tempfile::NamedTempFile::new() {
64 Ok(f) => f,
65 Err(_) => continue,
66 };
67
68 let mut buf = write_data;
70 buf[0] = (i & 0xFF) as u8;
71 buf[1] = ((i >> 8) & 0xFF) as u8;
72
73 if tmpfile.write_all(&buf).is_err() {
74 continue;
75 }
76 if tmpfile.flush().is_err() {
77 continue;
78 }
79 let file = tmpfile.as_file();
81 let t0 = std::time::Instant::now();
82 if file.sync_all().is_err() {
83 continue;
84 }
85 let elapsed = t0.elapsed();
86
87 timings.push(elapsed.as_nanos() as u64);
88 }
90
91 extract_timing_entropy(&timings, n_samples)
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn info() {
101 let src = FsyncJournalSource;
102 assert_eq!(src.name(), "fsync_journal");
103 assert_eq!(src.info().category, SourceCategory::IO);
104 assert!(!src.info().composite);
105 }
106
107 #[test]
108 #[ignore] fn collects_bytes() {
110 let src = FsyncJournalSource;
111 assert!(src.is_available());
112 let data = src.collect(64);
113 assert!(!data.is_empty());
114 assert!(data.len() <= 64);
115 }
116}