Skip to main content

openentropy_core/sources/frontier/
mach_ipc.rs

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