Skip to main content

openentropy_core/sources/thermal/
display_pll.rs

1//! Display PLL clock jitter — pixel clock oscillator phase noise.
2//!
3//! The display subsystem on Apple Silicon uses an independent PLL to generate
4//! the pixel clock (measured at ~533 MHz on Mac Mini M4). This PLL is
5//! electrically separate from both the CPU's 24 MHz crystal and the audio PLL.
6//!
7//! ## Entropy mechanism
8//!
9//! We issue CoreGraphics display queries (`CGDisplayCopyDisplayMode`,
10//! `CGDisplayModeGetRefreshRate`, pixel dimension lookups) that cross from the
11//! CPU clock domain into display subsystem timing paths. By reading CNTVCT_EL0
12//! (CPU crystal) before and after each query, we capture timing variation in the
13//! display path influenced by the display PLL's phase noise.
14//!
15//! The display PLL has its own thermal noise sources:
16//! - VCO transistor Johnson-Nyquist noise
17//! - Charge pump shot noise
18//! - Reference oscillator phase noise
19//! - Display cable/connector timing margin noise
20//!
21//! ## Why this is unique
22//!
23//! - **Third independent oscillator**: neither CPU crystal nor audio PLL
24//! - **High frequency PLL**: 533 MHz pixel clock means faster phase drift
25//! - **No special permissions**: CoreVideo is a standard framework
26//! - **Works headless**: Mac Mini always has a virtual display
27//!
28//! ## Tradeoff
29//!
30//! CoreGraphics query collection is slower than pure syscall-based sources.
31//! We oversample and extract timing deltas to recover usable entropy density.
32
33use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
34#[cfg(target_os = "macos")]
35use crate::sources::helpers::{extract_timing_entropy, read_cntvct};
36
37static DISPLAY_PLL_INFO: SourceInfo = SourceInfo {
38    name: "display_pll",
39    description: "Display PLL phase noise from pixel clock domain crossing",
40    physics: "Queries CoreGraphics display properties (mode, refresh rate, color space) \
41              that cross into the display PLL\u{2019}s clock domain (~533 MHz pixel clock). \
42              The display PLL is an independent oscillator from both the CPU crystal \
43              (24 MHz) and audio PLL (48 kHz). Phase noise arises from VCO transistor \
44              Johnson-Nyquist noise and charge pump shot noise in the display PLL. \
45              Reading CNTVCT_EL0 before and after each query captures the beat between \
46              CPU crystal and display PLL.",
47    category: SourceCategory::Thermal,
48    platform: Platform::MacOS,
49    requirements: &[Requirement::AppleSilicon],
50    entropy_rate_estimate: 4.0,
51    composite: false,
52    is_fast: true,
53};
54
55/// Display PLL phase noise entropy source.
56pub struct DisplayPllSource;
57
58/// CoreGraphics FFI for display property queries.
59#[cfg(target_os = "macos")]
60mod coregraphics {
61    use std::ffi::c_void;
62
63    // CGDirectDisplayID is u32 on macOS.
64    type CGDirectDisplayID = u32;
65    // CGDisplayModeRef is an opaque pointer.
66    type CGDisplayModeRef = *const c_void;
67
68    #[link(name = "CoreGraphics", kind = "framework")]
69    unsafe extern "C" {
70        fn CGMainDisplayID() -> CGDirectDisplayID;
71        fn CGDisplayCopyDisplayMode(display: CGDirectDisplayID) -> CGDisplayModeRef;
72        fn CGDisplayModeGetRefreshRate(mode: CGDisplayModeRef) -> f64;
73        fn CGDisplayModeGetPixelWidth(mode: CGDisplayModeRef) -> usize;
74        fn CGDisplayModeGetPixelHeight(mode: CGDisplayModeRef) -> usize;
75        fn CGDisplayModeRelease(mode: CGDisplayModeRef);
76        fn CGDisplayPixelsWide(display: CGDirectDisplayID) -> usize;
77        fn CGDisplayPixelsHigh(display: CGDirectDisplayID) -> usize;
78    }
79
80    /// Check if a display is available.
81    pub fn has_display() -> bool {
82        // SAFETY: CGMainDisplayID returns 0 if no display is available.
83        // It's a read-only query with no side effects.
84        unsafe { CGMainDisplayID() != 0 }
85    }
86
87    /// Query display mode properties, forcing a clock domain crossing.
88    /// Returns different property values based on `query_type` to exercise
89    /// different code paths in the display subsystem.
90    pub fn query_display_property(query_type: usize) -> u64 {
91        // SAFETY: All CG functions here are read-only queries on the main display.
92        // CGDisplayCopyDisplayMode returns a retained reference that we release.
93        unsafe {
94            let display = CGMainDisplayID();
95            if display == 0 {
96                return 0;
97            }
98
99            match query_type % 4 {
100                0 => {
101                    // Query display mode (refresh rate) — crosses into display PLL
102                    let mode = CGDisplayCopyDisplayMode(display);
103                    if mode.is_null() {
104                        return 0;
105                    }
106                    let rate = CGDisplayModeGetRefreshRate(mode);
107                    CGDisplayModeRelease(mode);
108                    rate.to_bits()
109                }
110                1 => {
111                    // Query pixel dimensions via display mode
112                    let mode = CGDisplayCopyDisplayMode(display);
113                    if mode.is_null() {
114                        return 0;
115                    }
116                    let w = CGDisplayModeGetPixelWidth(mode);
117                    let h = CGDisplayModeGetPixelHeight(mode);
118                    CGDisplayModeRelease(mode);
119                    (w as u64) ^ (h as u64).rotate_left(32)
120                }
121                2 => {
122                    // Direct pixel query (different code path)
123                    let w = CGDisplayPixelsWide(display);
124                    let h = CGDisplayPixelsHigh(display);
125                    (w as u64).wrapping_mul(h as u64)
126                }
127                _ => {
128                    // Mode + refresh combined
129                    let mode = CGDisplayCopyDisplayMode(display);
130                    if mode.is_null() {
131                        return 0;
132                    }
133                    let rate = CGDisplayModeGetRefreshRate(mode);
134                    let w = CGDisplayModeGetPixelWidth(mode);
135                    CGDisplayModeRelease(mode);
136                    rate.to_bits() ^ (w as u64)
137                }
138            }
139        }
140    }
141}
142
143impl EntropySource for DisplayPllSource {
144    fn info(&self) -> &SourceInfo {
145        &DISPLAY_PLL_INFO
146    }
147
148    fn is_available(&self) -> bool {
149        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
150        {
151            coregraphics::has_display()
152        }
153        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
154        {
155            false
156        }
157    }
158
159    fn collect(&self, n_samples: usize) -> Vec<u8> {
160        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
161        {
162            let _ = n_samples;
163            Vec::new()
164        }
165
166        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
167        {
168            let raw_count = n_samples * 4 + 64;
169            let mut beats: Vec<u64> = Vec::with_capacity(raw_count);
170
171            for i in 0..raw_count {
172                // Read CPU crystal counter before display domain crossing.
173                let counter_before = read_cntvct();
174
175                // Force a clock domain crossing into the display PLL.
176                let display_val = coregraphics::query_display_property(i);
177                std::hint::black_box(display_val);
178
179                // Read CPU crystal counter after display domain crossing.
180                let counter_after = read_cntvct();
181
182                // The duration in CNTVCT ticks captures the clock domain crossing
183                // time, modulated by the display PLL's phase. We don't XOR with
184                // counter_before (a monotonic counter creates NIST-detectable patterns).
185                let duration = counter_after.wrapping_sub(counter_before);
186                beats.push(duration);
187            }
188
189            extract_timing_entropy(&beats, n_samples)
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn info() {
200        let src = DisplayPllSource;
201        assert_eq!(src.name(), "display_pll");
202        assert_eq!(src.info().category, SourceCategory::Thermal);
203        assert!(!src.info().composite);
204    }
205
206    #[test]
207    fn physics_mentions_display() {
208        let src = DisplayPllSource;
209        assert!(src.info().physics.contains("display PLL"));
210        assert!(src.info().physics.contains("533 MHz"));
211        assert!(src.info().physics.contains("CNTVCT_EL0"));
212    }
213
214    #[test]
215    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
216    fn collects_bytes() {
217        let src = DisplayPllSource;
218        if src.is_available() {
219            let data = src.collect(64);
220            assert!(!data.is_empty());
221            assert!(data.len() <= 64);
222        }
223    }
224}