skip to content

What is a Mach port on macOS, and how do send and receive rights differ from a Unix file descriptor?

level: seniorimportance: nice to knowfreq 24%

answer

  1. kernel queue, never touched directly
  2. capability, not an open object
  3. one listener, many senders
  4. names are task-local integers
  5. peer death shows up as a dead name

basics

~20 s

A Mach port is a kernel-managed message queue that tasks reach through capabilities called rights. Exactly one task holds the receive right; any number may hold send rights. Like a file descriptor, a port name is task-local, but it names a capability to communicate rather than an open object.

solid answer

~50 s

A Mach port is an endpoint managed entirely by the kernel — a message queue you never touch directly. A task holds *rights* to it: a **receive right**, held by exactly one task at a time, which lets it dequeue messages; **send rights**, held by any number of tasks, which let them enqueue; and **send-once rights** for single replies. The port *name* a task uses is a small integer meaningful only inside that task, exactly like a file descriptor number. The differences matter, though: a descriptor names an open kernel object you read and write, while a port right is a capability to communicate with whoever holds the receive right, and rights themselves can be transferred inside ordinary `mach_msg` messages — no equivalent of a Unix-domain socket dance is required. When the receive right is destroyed, outstanding send rights become **dead names** and further sends fail, and holders can ask for a dead-name notification.

code

c · 21 lines
c
#include <mach/mach.h>
#include <stdio.h>

int main(void) {
    mach_port_t port;
    kern_return_t kr = mach_port_allocate(mach_task_self(),
                                          MACH_PORT_RIGHT_RECEIVE,
                                          &port);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "allocate failed: %d\n", kr);
        return 1;
    }
    kr = mach_port_insert_right(mach_task_self(), port, port,
                                MACH_MSG_TYPE_MAKE_SEND);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "insert right failed: %d\n", kr);
        return 1;
    }
    printf("port name in this task: %u\n", port);
    return 0;
}

go deeper

for a junior

Know that a Mach port is a kernel message queue, that tasks hold send or receive rights to it, and that the port name is a task-local number much like a file descriptor.

for a middle

Explain the one-receiver/many-senders rule, what send-once rights are for, and how rights are transferred inside ordinary Mach messages rather than through a socket mechanism.

for a senior

Reason about it in production: a hang is often a blocked message on a port, a vanished peer shows up as a dead name, and out-of-line memory changes the cost model of large payloads.

for a principal

Articulate why a capability model is the right foundation here — rights are unforgeable and delegable — and what it costs: task ports concentrate enormous authority, so the platform must ration them through privilege and entitlements.

## The object and the rights Mach IPC is capability-based. The object is a **port**: a kernel-owned message queue with no userspace representation at all. What a task actually possesses is a **right** to that port, and the right is what confers ability. - **Receive right** — held by **exactly one** task at a time. The holder is the server: it dequeues messages. Receive rights can be moved between tasks, but never copied, so the "who is listening" question always has one answer. - **Send right** — held by any number of tasks. It permits enqueueing messages, and nothing else. Send rights can be copied and handed out freely by whoever holds them. - **Send-once right** — permits exactly one message. This is the reply-port idiom: a client includes a send-once right in its request so the server can answer precisely once. - **Port set** — a receive-side grouping, letting one receive operation service many ports at once, the way a readiness API multiplexes descriptors. - **Dead name** — what a send right degrades into when the receive right goes away. Sends then fail, and a holder can register for a notification so it learns the peer died rather than discovering it on the next call. Each task has its own **port namespace**: the value it uses to refer to a right is a task-local integer, and the same underlying port may appear under different names in different tasks. A message is sent and received with the single `mach_msg` call, which both sends and receives depending on the options passed. ```c #include <mach/mach.h> int main(void) { mach_port_t p; if (mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p) != KERN_SUCCESS) return 1; /* manufacture a send right for the port we now receive on */ mach_port_insert_right(mach_task_self(), p, p, MACH_MSG_TYPE_MAKE_SEND); return 0; } ``` ## How this compares with file descriptors The similarities are real and worth stating first, because they anchor the explanation: - Both are **small integers meaningful only within one process/task** — the number 7 in two processes refers to different things. - Both are **kernel-mediated**: userspace never holds a pointer to the object. - Both can be **passed between processes**, so both are capabilities in the loose sense. The differences are where the interview lives: | | File descriptor | Mach port right | |---|---|---| | What it names | an open object (file, socket, pipe) | a capability on a message queue | | Direction | read and write on the same object | asymmetric: send vs receive are distinct rights | | Exclusivity | many processes may hold descriptors to one object with equal power | exactly one receive right; unlimited send rights | | Transfer | needs a Unix-domain socket and ancillary-data machinery | rights travel as descriptors inside an ordinary message | | Peer death | detected as EOF or a reset on I/O | send right becomes a dead name; explicit notification available | | Payload | a byte stream, structure imposed by the application | typed messages that can carry rights and out-of-line memory | That last row matters: a Mach message can carry **out-of-line memory**, which the kernel maps into the receiver rather than copying byte by byte, and can carry **port rights**, which is how a service hands a client access to another service. ## Why a backend or platform engineer should care The visible macOS IPC that applications use is layered on this. Services are found by name through a bootstrap namespace whose entries are ultimately send rights, and higher-level IPC on the platform is built over Mach messaging. When you debug a hang between two macOS processes, the primitive underneath is a port and a blocked `mach_msg`. The security angle is the one worth volunteering. Every task has a **task port**, and a send right to *another* task's port is close to total control over it — reading and writing its memory and creating threads in it. That is why obtaining another process's task port is privileged and gated by entitlements: it is the mechanism debuggers and profilers use, and it is exactly the capability an attacker wants. Framing it as "a task port is a capability, so the platform must ration it" shows you understand why the model is capability-based in the first place. ## What a strong answer sounds like Define the port as a kernel queue, enumerate the three rights with the one-receiver rule, note the task-local namespace, and then give one crisp contrast with descriptors — ideally that rights travel inside ordinary messages, or that a dead peer surfaces as a dead name rather than as EOF. Add the task-port security consequence and you have covered the ground the question exists to probe.

  • How many tasks can hold the receive right for a single Mach port, and why does that rule exist?
    Exactly one. The receive right can be moved between tasks but never copied, so there is always precisely one holder dequeuing messages. That single-receiver rule is what makes a port a well-defined service endpoint: senders know their message goes to one place, and the kernel knows unambiguously when the service is gone, at which point outstanding send rights become dead names.
  • Why is obtaining another process's task port a privileged operation?
    Because a send right to a task port is effectively total control over that task — reading and writing its memory and creating threads inside it. It is the capability debuggers and profilers rely on, which is exactly why the platform gates it behind privilege and entitlements. Handing it out freely would make process isolation meaningless.
  • What is a send-once right for?
    It permits exactly one message and is then consumed. It is the reply-port idiom: a client allocates a port, includes a send-once right in its request, and the server uses it to answer precisely once. It bounds the server's ability to talk back and lets the kernel guarantee the client sees either a reply or a notification, rather than an unbounded stream.

A receive right is the only key to a single mailbox; send rights are copies of the slot address that let anyone drop letters in. Handing someone a send right posts the address to them; destroying the mailbox does not silently swallow their letters, it makes the address dead.

saying these in an interview costs you the question

  • Saying several tasks can share one receive right
  • Treating a port right as just another file descriptor
  • Assuming Mach messages are only raw bytes
  • Believing a dead peer surfaces as EOF on send
  • Thinking task ports are freely obtainable by any process

context