Skip to main content

openentropy_core/sources/frontier/
mach_ipc.rs

1//! Mach IPC timing — entropy from complex Mach messages with OOL descriptors.
2
3#[cfg(target_os = "macos")]
4use std::sync::Arc;
5#[cfg(target_os = "macos")]
6use std::sync::atomic::{AtomicBool, Ordering};
7#[cfg(target_os = "macos")]
8use std::thread;
9
10use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
11#[cfg(target_os = "macos")]
12use crate::sources::helpers::{extract_timing_entropy, mach_time};
13
14/// Configuration for Mach IPC entropy collection.
15///
16/// # Example
17/// ```
18/// # use openentropy_core::sources::frontier::MachIPCConfig;
19/// let config = MachIPCConfig {
20///     num_ports: 16,               // more ports = more contention
21///     ool_size: 8192,              // larger OOL = more VM work
22///     use_complex_messages: true,  // OOL messages (recommended)
23/// };
24/// ```
25#[derive(Debug, Clone)]
26pub struct MachIPCConfig {
27    /// Number of Mach ports to round-robin across.
28    ///
29    /// More ports create more namespace contention (splay tree operations)
30    /// and varied queue depths. Each port is allocated with both receive
31    /// and send rights.
32    ///
33    /// **Range:** 1-64 (clamped to ≥1). **Default:** `8`
34    pub num_ports: usize,
35
36    /// Size of out-of-line (OOL) memory descriptors in bytes.
37    ///
38    /// OOL descriptors force the kernel to perform `vm_map_copyin`/`copyout`
39    /// operations, exercising page table updates and physical page allocation.
40    /// Larger sizes mean more VM work per message.
41    ///
42    /// **Range:** 1-65536. **Default:** `4096` (one page)
43    pub ool_size: usize,
44
45    /// Use complex messages with OOL descriptors (`true`) or simple port
46    /// allocate/deallocate (`false`, legacy behavior).
47    ///
48    /// Complex messages traverse deeper kernel paths and produce significantly
49    /// higher timing variance than simple port operations.
50    ///
51    /// **Default:** `true`
52    pub use_complex_messages: bool,
53}
54
55impl Default for MachIPCConfig {
56    fn default() -> Self {
57        Self {
58            num_ports: 8,
59            ool_size: 4096,
60            use_complex_messages: true,
61        }
62    }
63}
64
65/// Harvests timing jitter from Mach IPC using complex OOL messages.
66///
67/// # What it measures
68/// Nanosecond timing of `mach_msg()` sends with out-of-line memory descriptors,
69/// round-robined across a pool of Mach ports.
70///
71/// # Why it's entropic
72/// Complex Mach messages with OOL descriptors traverse deep kernel paths:
73/// - **OOL VM remapping** — `vm_map_copyin`/`vm_map_copyout` exercises page
74///   tables, physical page allocation, and TLB updates
75/// - **Port namespace contention** — round-robin across ports exercises the
76///   splay tree with timing dependent on tree depth and rebalancing
77/// - **Per-port lock contention** — `ipc_mqueue_send` acquires per-port locks
78/// - **Receiver thread wakeup** — cross-thread scheduling decisions affected
79///   by ALL runnable threads
80///
81/// # What makes it unique
82/// Mach IPC is unique to XNU/macOS. Unlike higher-level IPC (pipes, sockets),
83/// Mach messages go through XNU's `ipc_mqueue` subsystem with entirely different
84/// locking and scheduling paths. OOL descriptors add VM operations that no
85/// other entropy source exercises.
86///
87/// # Configuration
88/// See [`MachIPCConfig`] for tunable parameters. Key options:
89/// - `use_complex_messages`: OOL messages vs simple port ops (recommended: `true`)
90/// - `num_ports`: controls namespace contention level
91/// - `ool_size`: controls VM remapping workload per message
92#[derive(Default)]
93pub struct MachIPCSource {
94    /// Source configuration. Use `Default::default()` for recommended settings.
95    pub config: MachIPCConfig,
96}
97
98static MACH_IPC_INFO: SourceInfo = SourceInfo {
99    name: "mach_ipc",
100    description: "Mach port complex OOL message and VM remapping timing jitter",
101    physics: "Sends complex Mach messages with out-of-line (OOL) memory descriptors via \
102              mach_msg(), round-robining across multiple ports. OOL descriptors force kernel \
103              VM remapping (vm_map_copyin/copyout) which exercises page table operations. \
104              Round-robin across ports with varied queue depths creates namespace contention. \
105              Timing captures: OOL VM remap latency, port namespace splay tree operations, \
106              per-port lock contention, and cross-core scheduling nondeterminism.",
107    category: SourceCategory::IPC,
108    platform: Platform::MacOS,
109    requirements: &[],
110    entropy_rate_estimate: 2000.0,
111    composite: false,
112};
113
114impl EntropySource for MachIPCSource {
115    fn info(&self) -> &SourceInfo {
116        &MACH_IPC_INFO
117    }
118
119    fn is_available(&self) -> bool {
120        cfg!(target_os = "macos")
121    }
122
123    fn collect(&self, n_samples: usize) -> Vec<u8> {
124        #[cfg(not(target_os = "macos"))]
125        {
126            let _ = n_samples;
127            Vec::new()
128        }
129
130        #[cfg(target_os = "macos")]
131        {
132            let raw_count = n_samples * 4 + 64;
133            let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
134
135            // SAFETY: mach_task_self() returns the current task port (always valid).
136            let task = unsafe { mach_task_self() };
137            let num_ports = self.config.num_ports.max(1);
138
139            let mut ports: Vec<u32> = Vec::with_capacity(num_ports);
140            for _ in 0..num_ports {
141                let mut port: u32 = 0;
142                // SAFETY: mach_port_allocate allocates a receive right.
143                let kr = unsafe {
144                    mach_port_allocate(task, 1 /* MACH_PORT_RIGHT_RECEIVE */, &mut port)
145                };
146                if kr == 0 {
147                    // SAFETY: port is a valid receive right we just allocated.
148                    let kr2 = unsafe {
149                        mach_port_insert_right(
150                            task, port, port, 20, /* MACH_MSG_TYPE_MAKE_SEND */
151                        )
152                    };
153                    if kr2 == 0 {
154                        ports.push(port);
155                    } else {
156                        unsafe {
157                            mach_port_mod_refs(task, port, 1, -1);
158                        }
159                    }
160                }
161            }
162
163            if ports.is_empty() {
164                return self.collect_simple(n_samples);
165            }
166
167            if self.config.use_complex_messages {
168                let ool_size = self.config.ool_size.max(1);
169                let ool_buf = vec![0xBEu8; ool_size];
170
171                let stop = Arc::new(AtomicBool::new(false));
172                let stop2 = stop.clone();
173                let recv_ports = ports.clone();
174                let receiver = thread::spawn(move || {
175                    let mut recv_buf = vec![0u8; 1024 + ool_size * 2];
176                    while !stop2.load(Ordering::Relaxed) {
177                        for &port in &recv_ports {
178                            // SAFETY: recv_buf is large enough. Non-blocking receive.
179                            unsafe {
180                                let hdr = recv_buf.as_mut_ptr() as *mut MachMsgHeader;
181                                (*hdr).msgh_local_port = port;
182                                (*hdr).msgh_size = recv_buf.len() as u32;
183                                mach_msg(hdr, 2 | 0x100, 0, recv_buf.len() as u32, port, 0, 0);
184                            }
185                        }
186                        std::thread::yield_now();
187                    }
188                });
189
190                for i in 0..raw_count {
191                    let port = ports[i % ports.len()];
192
193                    let mut msg = MachMsgOOL::zeroed();
194                    msg.header.msgh_bits = 0x80000000 | 17; // COMPLEX | COPY_SEND
195                    msg.header.msgh_size = std::mem::size_of::<MachMsgOOL>() as u32;
196                    msg.header.msgh_remote_port = port;
197                    msg.header.msgh_local_port = 0;
198                    msg.header.msgh_id = i as i32;
199                    msg.body.msgh_descriptor_count = 1;
200                    msg.ool.address = ool_buf.as_ptr() as *mut _;
201                    msg.ool.size = ool_size as u32;
202                    msg.ool.deallocate = 0;
203                    msg.ool.copy = 1; // MACH_MSG_VIRTUAL_COPY
204                    msg.ool.ool_type = 1; // MACH_MSG_OOL_DESCRIPTOR
205
206                    let t0 = mach_time();
207                    // SAFETY: msg is properly initialized. MACH_SEND_TIMEOUT prevents blocking.
208                    unsafe {
209                        mach_msg(&mut msg.header, 1 | 0x80, msg.header.msgh_size, 0, 0, 10, 0);
210                    }
211                    let t1 = mach_time();
212                    timings.push(t1.wrapping_sub(t0));
213                }
214
215                stop.store(true, Ordering::Relaxed);
216                let _ = receiver.join();
217            } else {
218                for i in 0..raw_count {
219                    let t0 = mach_time();
220                    let base_port = ports[i % ports.len()];
221
222                    let mut new_port: u32 = 0;
223                    // SAFETY: standard Mach port operations.
224                    let kr = unsafe { mach_port_allocate(task, 1, &mut new_port) };
225                    if kr == 0 {
226                        unsafe {
227                            mach_port_deallocate(task, new_port);
228                            mach_port_mod_refs(task, new_port, 1, -1);
229                        }
230                    }
231                    unsafe {
232                        let mut ptype: u32 = 0;
233                        mach_port_type(task, base_port, &mut ptype);
234                    }
235                    let t1 = mach_time();
236                    timings.push(t1.wrapping_sub(t0));
237                }
238            }
239
240            for &port in &ports {
241                unsafe {
242                    mach_port_mod_refs(task, port, 1, -1);
243                }
244            }
245
246            extract_timing_entropy(&timings, n_samples)
247        }
248    }
249}
250
251#[cfg(target_os = "macos")]
252impl MachIPCSource {
253    fn collect_simple(&self, n_samples: usize) -> Vec<u8> {
254        let raw_count = n_samples * 4 + 64;
255        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
256        let task = unsafe { mach_task_self() };
257
258        for _ in 0..raw_count {
259            let t0 = mach_time();
260            let mut port: u32 = 0;
261            let kr = unsafe { mach_port_allocate(task, 1, &mut port) };
262            if kr == 0 {
263                unsafe {
264                    mach_port_deallocate(task, port);
265                    mach_port_mod_refs(task, port, 1, -1);
266                }
267            }
268            let t1 = mach_time();
269            timings.push(t1.wrapping_sub(t0));
270        }
271        extract_timing_entropy(&timings, n_samples)
272    }
273}
274
275// Mach message structures for complex OOL messages.
276#[cfg(target_os = "macos")]
277#[repr(C)]
278struct MachMsgHeader {
279    msgh_bits: u32,
280    msgh_size: u32,
281    msgh_remote_port: u32,
282    msgh_local_port: u32,
283    msgh_voucher_port: u32,
284    msgh_id: i32,
285}
286
287#[cfg(target_os = "macos")]
288#[repr(C)]
289struct MachMsgBody {
290    msgh_descriptor_count: u32,
291}
292
293#[cfg(target_os = "macos")]
294#[repr(C)]
295struct MachMsgOOLDescriptor {
296    address: *mut u8,
297    deallocate: u8,
298    copy: u8,
299    ool_type: u8,
300    _pad: u8,
301    size: u32,
302}
303
304#[cfg(target_os = "macos")]
305#[repr(C)]
306struct MachMsgOOL {
307    header: MachMsgHeader,
308    body: MachMsgBody,
309    ool: MachMsgOOLDescriptor,
310}
311
312// SAFETY: MachMsgOOL contains a raw pointer (ool.address), but we only use it
313// within a single thread's send operation where the pointed-to buffer is alive.
314#[cfg(target_os = "macos")]
315unsafe impl Send for MachMsgOOL {}
316
317#[cfg(target_os = "macos")]
318impl MachMsgOOL {
319    fn zeroed() -> Self {
320        // SAFETY: All-zeros is valid for this repr(C) struct.
321        unsafe { std::mem::zeroed() }
322    }
323}
324
325#[cfg(target_os = "macos")]
326unsafe extern "C" {
327    fn mach_task_self() -> u32;
328    fn mach_port_allocate(task: u32, right: i32, name: *mut u32) -> i32;
329    fn mach_port_deallocate(task: u32, name: u32) -> i32;
330    fn mach_port_mod_refs(task: u32, name: u32, right: i32, delta: i32) -> i32;
331    fn mach_port_insert_right(task: u32, name: u32, poly: u32, poly_poly: u32) -> i32;
332    fn mach_port_type(task: u32, name: u32, ptype: *mut u32) -> i32;
333    fn mach_msg(
334        msg: *mut MachMsgHeader,
335        option: i32,
336        send_size: u32,
337        rcv_size: u32,
338        rcv_name: u32,
339        timeout: u32,
340        notify: u32,
341    ) -> i32;
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn info() {
350        let src = MachIPCSource::default();
351        assert_eq!(src.name(), "mach_ipc");
352        assert_eq!(src.info().category, SourceCategory::IPC);
353        assert!(!src.info().composite);
354    }
355
356    #[test]
357    fn default_config() {
358        let config = MachIPCConfig::default();
359        assert_eq!(config.num_ports, 8);
360        assert_eq!(config.ool_size, 4096);
361        assert!(config.use_complex_messages);
362    }
363
364    #[test]
365    fn custom_config() {
366        let src = MachIPCSource {
367            config: MachIPCConfig {
368                num_ports: 4,
369                ool_size: 8192,
370                use_complex_messages: false,
371            },
372        };
373        assert_eq!(src.config.num_ports, 4);
374        assert!(!src.config.use_complex_messages);
375    }
376
377    #[test]
378    #[ignore] // Uses Mach ports
379    fn collects_bytes() {
380        let src = MachIPCSource::default();
381        assert!(src.is_available());
382        let data = src.collect(64);
383        assert!(!data.is_empty());
384        assert!(data.len() <= 64);
385    }
386
387    #[test]
388    #[ignore] // Uses Mach ports
389    fn simple_mode_collects_bytes() {
390        let src = MachIPCSource {
391            config: MachIPCConfig {
392                use_complex_messages: false,
393                ..MachIPCConfig::default()
394            },
395        };
396        assert!(!src.collect(64).is_empty());
397    }
398}