Skip to main content

openentropy_core/sources/frontier/
denormal_timing.rs

1//! Floating-point denormal timing — data-dependent microcode timing jitter.
2//!
3//! Denormalized floating-point numbers (between 0 and `f64::MIN_POSITIVE`)
4//! can cause data-dependent timing variation due to microcode assist or
5//! hardware handling differences. This source times blocks of denormal
6//! multiply-accumulate operations and extracts timing jitter.
7//!
8
9//! On Apple Silicon, denormal handling is fast (no microcode penalty),
10//! but residual pipeline state and cache effects still create jitter.
11
12use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
13use crate::sources::helpers::{extract_timing_entropy, mach_time};
14
15/// Number of floating-point operations per timing measurement.
16const OPS_PER_SAMPLE: usize = 100;
17
18static DENORMAL_TIMING_INFO: SourceInfo = SourceInfo {
19    name: "denormal_timing",
20    description: "Floating-point denormal multiply-accumulate timing jitter",
21    physics: "Times blocks of floating-point operations on denormalized values \
22              (magnitudes between 0 and f64::MIN_POSITIVE). Denormals may trigger \
23              microcode assists on some architectures, creating data-dependent timing. \
24              Even on Apple Silicon where denormal handling is fast in hardware, \
25              residual timing jitter comes from FPU pipeline state, cache line \
26              alignment, and memory controller arbitration.",
27    category: SourceCategory::Thermal,
28    platform: Platform::Any,
29    requirements: &[],
30    entropy_rate_estimate: 300.0,
31    composite: false,
32};
33
34/// Entropy source that harvests timing jitter from denormalized float operations.
35pub struct DenormalTimingSource;
36
37impl EntropySource for DenormalTimingSource {
38    fn info(&self) -> &SourceInfo {
39        &DENORMAL_TIMING_INFO
40    }
41
42    fn is_available(&self) -> bool {
43        true
44    }
45
46    fn collect(&self, n_samples: usize) -> Vec<u8> {
47        let raw_count = n_samples * 4 + 64;
48        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
49
50        // Pre-generate denormal values with varying mantissa patterns.
51        let mut lcg: u64 = mach_time() | 1;
52        let mut denormals = [0.0f64; OPS_PER_SAMPLE];
53        for d in denormals.iter_mut() {
54            lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
55            // Construct denormal: exponent bits = 0, random mantissa
56            let bits = lcg & 0x000F_FFFF_FFFF_FFFF_u64;
57            *d = f64::from_bits(bits);
58        }
59
60        for _ in 0..raw_count {
61            // Rotate denormal array slightly for per-iteration variation.
62            lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
63            let start_idx = (lcg >> 32) as usize % OPS_PER_SAMPLE;
64
65            let mut acc = denormals[start_idx];
66
67            let t0 = mach_time();
68            for i in 0..OPS_PER_SAMPLE {
69                let idx = (start_idx + i) % OPS_PER_SAMPLE;
70                acc *= denormals[idx];
71                acc += denormals[(idx + 1) % OPS_PER_SAMPLE];
72            }
73            let t1 = mach_time();
74
75            // Prevent dead code elimination.
76            std::hint::black_box(acc);
77            timings.push(t1.wrapping_sub(t0));
78        }
79
80        extract_timing_entropy(&timings, n_samples)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn info() {
90        let src = DenormalTimingSource;
91        assert_eq!(src.name(), "denormal_timing");
92        assert_eq!(src.info().category, SourceCategory::Thermal);
93        assert!(!src.info().composite);
94    }
95
96    #[test]
97    #[ignore] // Timing-dependent
98    fn collects_bytes() {
99        let src = DenormalTimingSource;
100        assert!(src.is_available());
101        let data = src.collect(64);
102        assert!(!data.is_empty());
103        assert!(data.len() <= 64);
104    }
105}