Skip to main content

openentropy_core/sources/ipc/
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::ipc::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: 2.0,
111    composite: false,
112    is_fast: true,
113};
114
115impl EntropySource for MachIPCSource {
116    fn info(&self) -> &SourceInfo {
117        &MACH_IPC_INFO
118    }
119
120    fn is_available(&self) -> bool {
121        cfg!(target_os = "macos")
122    }
123
124    fn collect(&self, n_samples: usize) -> Vec<u8> {
125        #[cfg(not(target_os = "macos"))]
126        {
127            let _ = n_samples;
128            Vec::new()
129        }
130
131        #[cfg(target_os = "macos")]
132        {
133            let raw_count = n_samples * 4 + 64;
134            let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
135
136            // SAFETY: mach_task_self() returns the current task port (always valid).
137            let task = unsafe { mach_task_self() };
138            let num_ports = self.config.num_ports.max(1);
139
140            let mut ports: Vec<u32> = Vec::with_capacity(num_ports);
141            for _ in 0..num_ports {
142                let mut port: u32 = 0;
143                // SAFETY: mach_port_allocate allocates a receive right.
144                let kr = unsafe {
145                    mach_port_allocate(task, 1 /* MACH_PORT_RIGHT_RECEIVE */, &mut port)
146                };
147                if kr == 0 {
148                    // SAFETY: port is a valid receive right we just allocated.
149                    let kr2 = unsafe {
150                        mach_port_insert_right(
151                            task, port, port, 20, /* MACH_MSG_TYPE_MAKE_SEND */
152                        )
153                    };
154                    if kr2 == 0 {
155                        ports.push(port);
156                    } else {
157                        unsafe {
158                            mach_port_mod_refs(task, port, 1, -1);
159                        }
160                    }
161                }
162            }
163
164            if ports.is_empty() {
165                return self.collect_simple(n_samples);
166            }
167
168            if self.config.use_complex_messages {
169                let ool_size = self.config.ool_size.max(1);
170                let ool_buf = vec![0xBEu8; ool_size];
171
172                let stop = Arc::new(AtomicBool::new(false));
173                let stop2 = stop.clone();
174                let recv_ports = ports.clone();
175                let receiver = thread::spawn(move || {
176                    let mut recv_buf = vec![0u8; 1024 + ool_size * 2];
177                    // Safety net: receiver exits after 30s even if stop is never set
178                    // (e.g. if the main thread panics between spawn and stop.store).
179                    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
180                    while !stop2.load(Ordering::Relaxed) && std::time::Instant::now() < deadline {
181                        for &port in &recv_ports {
182                            // SAFETY: recv_buf is large enough. Non-blocking receive.
183                            unsafe {
184                                let hdr = recv_buf.as_mut_ptr() as *mut MachMsgHeader;
185                                (*hdr).msgh_local_port = port;
186                                (*hdr).msgh_size = recv_buf.len() as u32;
187                                let kr =
188                                    mach_msg(hdr, 2 | 0x100, 0, recv_buf.len() as u32, port, 0, 0);
189                                // If we received a complex message, the kernel mapped OOL
190                                // data into our address space. Deallocate it to prevent
191                                // VM memory leaks proportional to message count.
192                                if kr == 0 && ((*hdr).msgh_bits & 0x80000000) != 0 {
193                                    let recv_ool_ptr = recv_buf.as_ptr().add(
194                                        std::mem::size_of::<MachMsgHeader>()
195                                            + std::mem::size_of::<MachMsgBody>(),
196                                    )
197                                        as *const MachMsgOOLDescriptor;
198                                    let recv_ool = std::ptr::read_unaligned(recv_ool_ptr);
199                                    let addr = recv_ool.address as usize;
200                                    let size = recv_ool.size as usize;
201                                    if addr != 0 && size > 0 {
202                                        vm_deallocate(mach_task_self(), addr, size);
203                                    }
204                                }
205                            }
206                        }
207                        std::thread::yield_now();
208                    }
209                });
210
211                for i in 0..raw_count {
212                    let port = ports[i % ports.len()];
213
214                    let mut msg = MachMsgOOL::zeroed();
215                    msg.header.msgh_bits = 0x80000000 | 17; // COMPLEX | COPY_SEND
216                    msg.header.msgh_size = std::mem::size_of::<MachMsgOOL>() as u32;
217                    msg.header.msgh_remote_port = port;
218                    msg.header.msgh_local_port = 0;
219                    msg.header.msgh_id = i as i32;
220                    msg.body.msgh_descriptor_count = 1;
221                    msg.ool.address = ool_buf.as_ptr() as *mut _;
222                    msg.ool.size = ool_size as u32;
223                    msg.ool.deallocate = 0;
224                    msg.ool.copy = 1; // MACH_MSG_VIRTUAL_COPY
225                    msg.ool.ool_type = 1; // MACH_MSG_OOL_DESCRIPTOR
226
227                    let t0 = mach_time();
228                    // SAFETY: msg is properly initialized. MACH_SEND_TIMEOUT prevents blocking.
229                    unsafe {
230                        mach_msg(&mut msg.header, 1 | 0x80, msg.header.msgh_size, 0, 0, 10, 0);
231                    }
232                    let t1 = mach_time();
233                    timings.push(t1.wrapping_sub(t0));
234                }
235
236                stop.store(true, Ordering::Relaxed);
237                let _ = receiver.join();
238            } else {
239                for i in 0..raw_count {
240                    let t0 = mach_time();
241                    let base_port = ports[i % ports.len()];
242
243                    let mut new_port: u32 = 0;
244                    // SAFETY: standard Mach port operations.
245                    let kr = unsafe { mach_port_allocate(task, 1, &mut new_port) };
246                    if kr == 0 {
247                        // Drop the receive right. mach_port_allocate with
248                        // MACH_PORT_RIGHT_RECEIVE creates a receive right only;
249                        // use mod_refs to release it (not mach_port_deallocate,
250                        // which is for send/send-once rights).
251                        unsafe {
252                            mach_port_mod_refs(task, new_port, 1, -1);
253                        }
254                    }
255                    unsafe {
256                        let mut ptype: u32 = 0;
257                        mach_port_type(task, base_port, &mut ptype);
258                    }
259                    let t1 = mach_time();
260                    timings.push(t1.wrapping_sub(t0));
261                }
262            }
263
264            for &port in &ports {
265                unsafe {
266                    // Release the send right created by mach_port_insert_right.
267                    mach_port_mod_refs(task, port, 0 /* MACH_PORT_RIGHT_SEND */, -1);
268                    // Release the receive right created by mach_port_allocate.
269                    mach_port_mod_refs(task, port, 1 /* MACH_PORT_RIGHT_RECEIVE */, -1);
270                }
271            }
272
273            extract_timing_entropy(&timings, n_samples)
274        }
275    }
276}
277
278#[cfg(target_os = "macos")]
279impl MachIPCSource {
280    fn collect_simple(&self, n_samples: usize) -> Vec<u8> {
281        let raw_count = n_samples * 4 + 64;
282        let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
283        let task = unsafe { mach_task_self() };
284
285        for _ in 0..raw_count {
286            let t0 = mach_time();
287            let mut port: u32 = 0;
288            let kr = unsafe { mach_port_allocate(task, 1, &mut port) };
289            if kr == 0 {
290                // Drop the receive right directly via mod_refs.
291                // mach_port_deallocate is for send rights and would leave
292                // the port name invalid before the subsequent mod_refs call.
293                unsafe {
294                    mach_port_mod_refs(task, port, 1, -1);
295                }
296            }
297            let t1 = mach_time();
298            timings.push(t1.wrapping_sub(t0));
299        }
300        extract_timing_entropy(&timings, n_samples)
301    }
302}
303
304// Mach message structures for complex OOL messages.
305#[cfg(target_os = "macos")]
306#[repr(C)]
307struct MachMsgHeader {
308    msgh_bits: u32,
309    msgh_size: u32,
310    msgh_remote_port: u32,
311    msgh_local_port: u32,
312    msgh_voucher_port: u32,
313    msgh_id: i32,
314}
315
316#[cfg(target_os = "macos")]
317#[repr(C)]
318struct MachMsgBody {
319    msgh_descriptor_count: u32,
320}
321
322#[cfg(target_os = "macos")]
323#[repr(C)]
324struct MachMsgOOLDescriptor {
325    address: *mut u8,
326    deallocate: u8,
327    copy: u8,
328    _pad: u8,
329    ool_type: u8, // mach_msg_descriptor_type_t — must be last in this group
330    size: u32,
331}
332
333#[cfg(target_os = "macos")]
334#[repr(C)]
335struct MachMsgOOL {
336    header: MachMsgHeader,
337    body: MachMsgBody,
338    ool: MachMsgOOLDescriptor,
339}
340
341// MachMsgOOL contains a raw pointer (ool.address) and is intentionally !Send.
342// It is only used on the same thread that owns the pointed-to buffer.
343
344#[cfg(target_os = "macos")]
345impl MachMsgOOL {
346    fn zeroed() -> Self {
347        // SAFETY: All-zeros is valid for this repr(C) struct.
348        unsafe { std::mem::zeroed() }
349    }
350}
351
352#[cfg(target_os = "macos")]
353unsafe extern "C" {
354    fn mach_task_self() -> u32;
355    fn mach_port_allocate(task: u32, right: i32, name: *mut u32) -> i32;
356    fn mach_port_mod_refs(task: u32, name: u32, right: i32, delta: i32) -> i32;
357    fn mach_port_insert_right(task: u32, name: u32, poly: u32, poly_poly: u32) -> i32;
358    fn mach_port_type(task: u32, name: u32, ptype: *mut u32) -> i32;
359    fn mach_msg(
360        msg: *mut MachMsgHeader,
361        option: i32,
362        send_size: u32,
363        rcv_size: u32,
364        rcv_name: u32,
365        timeout: u32,
366        notify: u32,
367    ) -> i32;
368    fn vm_deallocate(target: u32, addr: usize, size: usize) -> i32;
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn info() {
377        let src = MachIPCSource::default();
378        assert_eq!(src.name(), "mach_ipc");
379        assert_eq!(src.info().category, SourceCategory::IPC);
380        assert!(!src.info().composite);
381    }
382
383    #[test]
384    fn default_config() {
385        let config = MachIPCConfig::default();
386        assert_eq!(config.num_ports, 8);
387        assert_eq!(config.ool_size, 4096);
388        assert!(config.use_complex_messages);
389    }
390
391    #[test]
392    fn custom_config() {
393        let src = MachIPCSource {
394            config: MachIPCConfig {
395                num_ports: 4,
396                ool_size: 8192,
397                use_complex_messages: false,
398            },
399        };
400        assert_eq!(src.config.num_ports, 4);
401        assert!(!src.config.use_complex_messages);
402    }
403
404    #[test]
405    #[ignore] // Uses Mach ports
406    fn collects_bytes() {
407        let src = MachIPCSource::default();
408        assert!(src.is_available());
409        let data = src.collect(64);
410        assert!(!data.is_empty());
411        assert!(data.len() <= 64);
412    }
413
414    #[test]
415    #[ignore] // Uses Mach ports
416    fn simple_mode_collects_bytes() {
417        let src = MachIPCSource {
418            config: MachIPCConfig {
419                use_complex_messages: false,
420                ..MachIPCConfig::default()
421            },
422        };
423        assert!(!src.collect(64).is_empty());
424    }
425}