Skip to main content

openentropy_core/sources/frontier/
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: 2500.0,
51    composite: false,
52};
53
54/// Display PLL phase noise entropy source.
55pub struct DisplayPllSource;
56
57/// CoreGraphics FFI for display property queries.
58#[cfg(target_os = "macos")]
59mod coregraphics {
60    use std::ffi::c_void;
61
62    // CGDirectDisplayID is u32 on macOS.
63    type CGDirectDisplayID = u32;
64    // CGDisplayModeRef is an opaque pointer.
65    type CGDisplayModeRef = *const c_void;
66
67    #[link(name = "CoreGraphics", kind = "framework")]
68    unsafe extern "C" {
69        fn CGMainDisplayID() -> CGDirectDisplayID;
70        fn CGDisplayCopyDisplayMode(display: CGDirectDisplayID) -> CGDisplayModeRef;
71        fn CGDisplayModeGetRefreshRate(mode: CGDisplayModeRef) -> f64;
72        fn CGDisplayModeGetPixelWidth(mode: CGDisplayModeRef) -> usize;
73        fn CGDisplayModeGetPixelHeight(mode: CGDisplayModeRef) -> usize;
74        fn CGDisplayModeRelease(mode: CGDisplayModeRef);
75        fn CGDisplayPixelsWide(display: CGDirectDisplayID) -> usize;
76        fn CGDisplayPixelsHigh(display: CGDirectDisplayID) -> usize;
77    }
78
79    /// Check if a display is available.
80    pub fn has_display() -> bool {
81        // SAFETY: CGMainDisplayID returns 0 if no display is available.
82        // It's a read-only query with no side effects.
83        unsafe { CGMainDisplayID() != 0 }
84    }
85
86    /// Query display mode properties, forcing a clock domain crossing.
87    /// Returns different property values based on `query_type` to exercise
88    /// different code paths in the display subsystem.
89    pub fn query_display_property(query_type: usize) -> u64 {
90        // SAFETY: All CG functions here are read-only queries on the main display.
91        // CGDisplayCopyDisplayMode returns a retained reference that we release.
92        unsafe {
93            let display = CGMainDisplayID();
94            if display == 0 {
95                return 0;
96            }
97
98            match query_type % 4 {
99                0 => {
100                    // Query display mode (refresh rate) — crosses into display PLL
101                    let mode = CGDisplayCopyDisplayMode(display);
102                    if mode.is_null() {
103                        return 0;
104                    }
105                    let rate = CGDisplayModeGetRefreshRate(mode);
106                    CGDisplayModeRelease(mode);
107                    rate.to_bits()
108                }
109                1 => {
110                    // Query pixel dimensions via display mode
111                    let mode = CGDisplayCopyDisplayMode(display);
112                    if mode.is_null() {
113                        return 0;
114                    }
115                    let w = CGDisplayModeGetPixelWidth(mode);
116                    let h = CGDisplayModeGetPixelHeight(mode);
117                    CGDisplayModeRelease(mode);
118                    (w as u64) ^ (h as u64).rotate_left(32)
119                }
120                2 => {
121                    // Direct pixel query (different code path)
122                    let w = CGDisplayPixelsWide(display);
123                    let h = CGDisplayPixelsHigh(display);
124                    (w as u64).wrapping_mul(h as u64)
125                }
126                _ => {
127                    // Mode + refresh combined
128                    let mode = CGDisplayCopyDisplayMode(display);
129                    if mode.is_null() {
130                        return 0;
131                    }
132                    let rate = CGDisplayModeGetRefreshRate(mode);
133                    let w = CGDisplayModeGetPixelWidth(mode);
134                    CGDisplayModeRelease(mode);
135                    rate.to_bits() ^ (w as u64)
136                }
137            }
138        }
139    }
140}
141
142impl EntropySource for DisplayPllSource {
143    fn info(&self) -> &SourceInfo {
144        &DISPLAY_PLL_INFO
145    }
146
147    fn is_available(&self) -> bool {
148        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
149        {
150            coregraphics::has_display()
151        }
152        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
153        {
154            false
155        }
156    }
157
158    fn collect(&self, n_samples: usize) -> Vec<u8> {
159        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
160        {
161            let _ = n_samples;
162            Vec::new()
163        }
164
165        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
166        {
167            let raw_count = n_samples * 4 + 64;
168            let mut beats: Vec<u64> = Vec::with_capacity(raw_count);
169
170            for i in 0..raw_count {
171                // Read CPU crystal counter before display domain crossing.
172                let counter_before = read_cntvct();
173
174                // Force a clock domain crossing into the display PLL.
175                let display_val = coregraphics::query_display_property(i);
176                std::hint::black_box(display_val);
177
178                // Read CPU crystal counter after display domain crossing.
179                let counter_after = read_cntvct();
180
181                // The duration in CNTVCT ticks captures the clock domain crossing
182                // time, modulated by the display PLL's phase. We don't XOR with
183                // counter_before (a monotonic counter creates NIST-detectable patterns).
184                let duration = counter_after.wrapping_sub(counter_before);
185                beats.push(duration);
186            }
187
188            extract_timing_entropy(&beats, n_samples)
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn info() {
199        let src = DisplayPllSource;
200        assert_eq!(src.name(), "display_pll");
201        assert_eq!(src.info().category, SourceCategory::Thermal);
202        assert!(!src.info().composite);
203    }
204
205    #[test]
206    fn physics_mentions_display() {
207        let src = DisplayPllSource;
208        assert!(src.info().physics.contains("display PLL"));
209        assert!(src.info().physics.contains("533 MHz"));
210        assert!(src.info().physics.contains("CNTVCT_EL0"));
211    }
212
213    #[test]
214    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
215    fn collects_bytes() {
216        let src = DisplayPllSource;
217        if src.is_available() {
218            let data = src.collect(64);
219            assert!(!data.is_empty());
220            assert!(data.len() <= 64);
221        }
222    }
223}