Mach ports: how every macOS process actually talks to another
Tasks, ports, messages, and rights — the IPC primitive that quietly carries every IPC on your Mac, from XPC to drag-and-drop.
Open Activity Monitor. Every line in it is a task. Pick any two and ask: how do they talk? Pipes are rare. Shared memory is rarer. UNIX sockets show up sometimes. The honest answer, for almost every interaction on your Mac, is Mach IPC — and the unit of currency is the port.
This article is a tour of that mechanism: what a port is, what a message looks like on the wire, what the kernel does when one is sent, and where in XNU you can read the code yourself.
A task is not a process (quite)
Mach predates BSD process semantics by about a decade — Rashid's 1986 Carnegie Mellon paper described tasks, threads, and ports before the BSD union showed up. In XNU, every BSD proc has a paired Mach task, and every BSD thread has a paired Mach thread. The task owns memory and ports; the thread owns execution state.
The first thing that should jar your intuition is this: a task does not have a name. You don't reach Finder by string. You reach it by port right — a numeric capability that the kernel hands you and remembers on your behalf.
A port is a kernel-owned mailbox
A Mach port is two things at once:
- A message queue, owned by the kernel.
- A set of rights — capabilities to interact with that queue, held by tasks.
The queue lives in kernel memory; nothing in userspace can touch it directly. Tasks see only their port-right table, indexed by a per-task mach_port_name_t (a 32-bit integer that looks suspiciously like a file descriptor — and isn't an accident).
The three rights you'll meet over and over:
- Receive right: there is exactly one per port, ever. The task that holds it dequeues messages. Lose it (task dies) and the port is destroyed.
- Send right: any task with one can enqueue a message. Many tasks can hold a send right to the same port simultaneously.
- Send-once right: a one-shot send right. Common as a reply token: "answer me on this port, then forget the right exists."
Capabilities, not identities. If you hold the send right, you can talk to whatever's on the other end. The kernel tracks who holds what.
apple-oss-distributions/xnuosfmk/ipc/ipc_right.cThe kernel routines that issue, copy, and destroy port rights.View on GitHub(line —)Sending a message
A Mach message is a header followed by an optional body. The header is small and fixed:
typedef struct {
mach_msg_bits_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t;
apple-oss-distributions/xnuosfmk/mach/message.h:430Layout of mach_msg_header_t. Read this once and a lot of the kernel will start to make sense.View on GitHub(line 430)
The two ports in the header — remote_port (destination) and local_port (where to reply) — are the entire addressing scheme. There is no "process ID", no DNS, no hostname. There is a number on a task's port table that the kernel resolves to a queue.
If the body is set, it carries descriptors — typed records that can transfer port rights between tasks, hand off memory pages, or pass an out-of-line array. This is where Mach gets its real power: a message is not just data, it's a way to move capabilities and memory between tasks atomically.
apple-oss-distributions/xnuosfmk/ipc/mach_msg.c:1mach_msg_overwrite_trap — the entry point every send goes through.View on GitHub(line 1)The send path, at a glance:
- The caller traps into the kernel via
mach_msg_trap(one of the original Mach traps; system call numbers-31and friends inosfmk/mach/syscall_sw.h). - The kernel validates the descriptors and copies any inline payload.
- Out-of-line descriptors trigger a copy-on-write virtual memory transfer — the receiver gets a mapping, not a memcpy.
- The message is appended to the destination port's queue.
- If a thread is blocked in
mach_msg_receive, it's unblocked and handed the message.
Why "everything is a port" actually pays off
Once you've seen the primitive, the upper layers fall into place fast:
bootstrapis a Mach service that maps string names ("com.apple.SystemUIServer") to send rights, so launched processes can find services they didn't fork from.- XPC is a layered protocol on top of Mach messages, with a stricter codec and a queue-aware API; the kernel just sees ports and messages.
- MIG ("Mach Interface Generator") is the IDL most kernel servers expose —
mach_port_allocate,task_get_special_port, everyhost_*call. It generates marshal/unmarshal stubs aroundmach_msg. - Signals, in part, ride the same machinery:
task_set_special_port(TASK_EXCEPTION_PORT, …)is how a debugger gets first crack at faults.
Things that surprise newcomers
- Send rights have user-reference counts. Copying one bumps the count; the kernel reclaims the right when it hits zero. This is why
mach_port_mod_refsexists, and why over-releasing a port aborts the task. - The 64-bit address space made Mach descriptors larger, not smaller — porting MIG stubs across that boundary was a multi-year story in the diffs.
mach_msg_receiveblocks the calling thread. If the queue's empty and the timeout isMACH_MSG_TIMEOUT_NONE, the thread sleeps until something arrives — the kernel checks if there's any possibility of a future message (any send rights still extant), and if not, returnsMACH_RCV_PORT_DIED.- Userspace-visible port names are per-task: the same kernel port is name
0x1003in task A and0x2807in task B.
What to read next
Start with the structures, then chase a send all the way through:
apple-oss-distributions/xnuosfmk/ipc/ipc_kmsg.cipc_kmsg — the kernel's internal representation of a message in flight.View on GitHub(line —) apple-oss-distributions/xnuosfmk/ipc/ipc_mqueue.cipc_mqueue — how a port's queue is implemented and woken.View on GitHub(line —) apple-oss-distributions/xnuosfmk/kern/ipc_tt.cTask / thread / host port lookups — how you actually get a send right to anything.View on GitHub(line —)
Then read BSD's proc and notice it's a struct that holds a pointer to task — not the other way around. That asymmetry tells you which layer was there first.