macOS IPC — Mach IPC
What You Will Learn
- How Mach IPC works and what its core components are
- How to send and receive messages between processes
- How IOKit allows userspace to communicate with kernel drivers
- What PAC is and how it protects pointers on macOS/iOS
What Is It?
Mach IPC is the inter-process communication mechanism at the heart of macOS and iOS (XNU kernel). It enables tasks (processes) to exchange information through ports asynchronously.
Understanding Mach IPC is essential for macOS/iOS security research, privilege escalation, and kernel exploitation.
Core Components
| Component | Description |
|---|---|
| Ports | Kernel-managed communication channels, similar to pipes |
| Port Rights | Permissions that control how processes interact with ports |
| Messages | Structured data units exchanged between ports |
| Service | A named port registered with the bootstrap server |
| Bootstrap Server | launchd — handles service registration and discovery |
Setting Up a Server and Client
Process A (Server)
mach_port_t server_port;
// Create port with RECEIVE right
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
// Add SEND right
mach_port_insert_right(mach_task_self(), server_port, server_port, MACH_MSG_TYPE_MAKE_SEND);
// Register with launchd
bootstrap_check_in(bootstrap_port, "com.example.service", server_port);
Process B (Client)
mach_port_t client_port;
// Look up the service
bootstrap_look_up(bootstrap_port, "com.example.service", &client_port);
Mach Message Header
typedef struct {
mach_msg_bits_t msgh_bits; // options and port right dispositions
mach_msg_size_t msgh_size; // total message size including header
mach_port_t msgh_remote_port; // destination port when sending
mach_port_t msgh_local_port; // reply port
mach_port_name_t msgh_voucher_port; // optional Mach voucher
mach_msg_id_t msgh_id; // user-defined message ID
} mach_msg_header_t;
Port Dispositions
A port disposition defines how the port right is passed in the message:
| Disposition | Meaning |
|---|---|
MACH_MSG_TYPE_MOVE_SEND | Move the send right to receiver (sender loses it) |
MACH_MSG_TYPE_COPY_SEND | Copy the send right to receiver (sender keeps it) |
MACH_MSG_TYPE_MOVE_RECEIVE | Move the receive right to receiver |
MACH_MSG_TYPE_MAKE_SEND | Give a new send right based on a receive right the sender holds |
Bidirectional Message — Full Example
Get Service Port
mach_port_t port;
bootstrap_look_up(bootstrap_port, "com.example.service", &port);
Create Local Reply Port
mach_port_t replyPort;
mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &replyPort);
Insert Send Right on Reply Port
mach_port_insert_right(task, replyPort, replyPort, MACH_MSG_TYPE_MAKE_SEND);
Prepare and Send Message
Message message = {0};
message.header.msgh_remote_port = port;
message.header.msgh_local_port = replyPort;
// Port dispositions
message.header.msgh_bits = MACH_MSGH_BITS_SET(
MACH_MSG_TYPE_COPY_SEND, // remote: send right
MACH_MSG_TYPE_MAKE_SEND_ONCE, // local: reply right (one-time)
0, 0
);
MACH_MSG_TYPE_COPY_SEND: use the send right but do not remove it from this processMACH_MSG_TYPE_MAKE_SEND_ONCE: allow receiver to reply once, then the right is destroyed
Complex Messages
A complex Mach message can carry port descriptors in the body (in addition to the header ports):
typedef struct {
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_port_descriptor_t descriptor;
} PortMessage;
Port Descriptor (in message body)
typedef struct {
mach_port_t name;
mach_msg_size_t pad1;
unsigned int pad2 : 16;
mach_msg_type_name_t disposition : 8;
mach_msg_descriptor_type_t type : 8;
} mach_msg_port_descriptor_t;
OOL (Out-Of-Line) Messages
OOL messages carry memory references rather than inline data:
typedef struct {
mach_msg_header_t header;
mach_msg_size_t msgh_descriptor_count;
mach_msg_ool_descriptor_t descriptor;
} OOLMachMessage;
Controlling Another Task
Two ways to control another task’s memory:
- Target sends task port: The target process sends a send right to its task port
- Privileged access: Root + SIP allows
task_for_pid. Requires thecom.apple.security.cs.debuggerentitlement
Exception Ports
Any process with the right Mach port access can register as an exception handler for another process:
// Allocate exception port
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &exception_port);
mach_port_insert_right(mach_task_self(), exception_port, exception_port, MACH_MSG_TYPE_MAKE_SEND);
// Register as exception handler for this task
task_set_exception_ports(mach_task_self(), EXC_MASK_ALL, exception_port,
EXCEPTION_DEFAULT, THREAD_STATE_NONE);
IOKit
IOKit is the framework for communicating with kernel drivers (kexts) from userspace.
// Find and connect to a driver
io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault,
IOServiceMatching("DriverName"));
io_connect_t connect;
IOServiceOpen(service, mach_task_self(), 1, &connect);
// Call a driver method
IOConnectCallMethod(connect, 0, // selector
NULL, 0, NULL, 0, NULL, NULL, NULL, NULL);
IOServiceClose(connect);
IOObjectRelease(service);
PAC (Pointer Authentication Codes)
See Topic79 — ARM64 Assembly for a detailed PAC reference.