Skip to main content

openentropy_core/sources/io/
fsync_journal.rs

1//! Filesystem journal commit timing — full storage stack entropy.
2//!
3//! APFS uses copy-on-write with a journal. Each fsync crosses:
4//!   CPU → filesystem → NVMe controller → NAND flash → back
5//!
6//! Each layer adds independent noise:
7//! - Checksum computation (CPU pipeline state)
8//! - NVMe command queuing and arbitration
9//! - Flash cell program timing (temperature-dependent)
10//! - B-tree update (memory allocation nondeterminism)
11//! - Barrier flush (controller firmware scheduling)
12//!
13//! Different from disk_io because this specifically measures the full
14//! journal commit path, not just raw block reads.
15//!
16
17use 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
39/// Entropy source from filesystem journal commit timing.
40pub 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            // Create a new temp file each iteration to exercise the full
62            // APFS allocation + B-tree insert + journal commit path.
63            let mut tmpfile = match tempfile::NamedTempFile::new() {
64                Ok(f) => f,
65                Err(_) => continue,
66            };
67
68            // Vary the first bytes to prevent APFS deduplication.
69            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            // Time only the fsync (journal commit) — not the write+flush above.
80            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            // tmpfile is automatically deleted on drop.
89        }
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] // I/O dependent
109    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}