Skip to content
macOSDeep Dive Published Updated 6 min readViews unavailable

XPC Services: How macOS Processes Talk to Each Other Securely

The IPC framework behind most of macOS's privilege separation, built on Mach IPC but designed to make secure, sandboxed communication the easy default.

A huge amount of what macOS does behind the scenes involves one process asking another, more privileged (or differently privileged) process to do something on its behalf — a sandboxed application asking for file access outside its container, a system service performing a privileged operation for an unprivileged client — and XPC is the framework Apple built specifically to make that kind of cross-process communication both convenient for developers and secure by construction.

The underlying primitive: Mach ports and messages

XPC is built directly on top of Mach IPC — specifically, Mach ports (kernel-managed communication endpoints) and Mach messages (the data sent through them). Mach’s IPC model was designed from the start around the idea of communication as a first-class, kernel-mediated primitive rather than something layered on top of shared memory or files, which makes it a genuinely solid foundation for a security-sensitive communication framework: the kernel itself is the arbiter of which process can send to which port, rather than relying on cooperating processes to police access themselves.

What XPC adds on top of raw Mach IPC

Working with raw Mach ports and messages directly is low-level and easy to get wrong — manual message construction, port rights management, and serialization are all things a developer would otherwise have to handle correctly by hand. XPC wraps this in a much higher-level API: structured, dictionary-like messages (XPC objects) that serialize and deserialize automatically, connection objects that manage the underlying Mach port lifecycle, and — critically for security — automatic handling of connection validation, so a service can straightforwardly verify who’s actually connecting to it before trusting anything in the message.

Why this matters for sandboxing specifically

macOS’s App Sandbox restricts what a sandboxed application can directly do — access arbitrary files, reach arbitrary network hosts, control other processes — and XPC is the mechanism that lets a sandboxed app still get privileged work done without simply punching holes in its own sandbox to do it directly. Instead, the sandboxed app talks over XPC to a separate, unsandboxed (or differently-privileged) helper process or system service, which performs the actual privileged operation and returns just the result. The sandboxed app’s own process never gains the broader privilege itself — it just receives the outcome of an operation performed by something else that has it.

The security boundary this creates

This pattern — a tightly scoped, single-purpose XPC service doing one specific privileged thing on behalf of less-privileged callers — is deliberately different from simply granting broad privilege to a large, general-purpose process. If an XPC service is compromised, the damage is bounded by exactly what that specific service was designed to do, not by whatever the calling application could otherwise reach. This is why macOS system architecture tends toward many small, single-purpose XPC services rather than a few large, broadly-privileged daemons — it’s a direct, deliberate application of the principle of least privilege at the process-architecture level, not just at the file-permission level.

Where developers actually see this

Any macOS app using system frameworks for privileged operations — printing, certain camera or microphone access flows, background app extensions — is very likely going through an XPC-based service under the hood, whether or not the developer explicitly wrote any XPC code themselves; a great deal of Apple’s own system frameworks use XPC internally to reach privileged system services on the calling application’s behalf, entirely transparently. Developers building their own privilege-separated app architecture (a sandboxed main app plus a separate, differently-entitled helper) use the NSXPCConnection/xpc_connection_t APIs directly, which handle the connection lifecycle, message serialization, and basic validation automatically rather than requiring manual Mach port management.

Why this design has held up

The core idea — cheap, kernel-mediated, structured message passing as the connective tissue between deliberately narrow, single-purpose privileged services — has scaled from its original use inside macOS itself to become the standard pattern Apple recommends for third-party app architecture as sandbox and entitlement requirements have tightened over time. It’s a clear case of an IPC mechanism originally built for one specific internal purpose (letting Mach’s microkernel-derived IPC serve XNU’s actual privilege-separation needs) turning out to generalize well to an entire platform’s approach to secure inter-process communication, years after the underlying Mach IPC primitives were first designed.

How launchd fits into the XPC lifecycle

An XPC service isn’t typically a constantly-running background process waiting for connections — launchd is what actually starts an XPC service’s process on demand, the moment a client first attempts to connect to it, and can let the service’s process exit again once idle, restarting it transparently the next time a client connects. This on-demand activation, inherited conceptually from inetd’s socket-triggered launching but applied to XPC’s structured connections instead of raw sockets, is a meaningful resource-efficiency property distinct from XPC’s security benefits: the system doesn’t need to keep hundreds of narrowly-scoped helper services resident in memory simultaneously just in case they’re needed, since launchd’s on-demand activation means most of them only actually exist as running processes for the brief windows they’re genuinely doing something.

Connection validation: knowing who’s actually on the other end

A privileged XPC service accepting connections from potentially many different, less-privileged clients needs a reliable way to know exactly which process (and, critically, which specific code-signed identity) is actually connecting, before trusting anything in an incoming message. XPC surfaces this through the connection object’s audit token, which a service can check against an expected code-signing requirement — confirming the connecting process is signed by a specific expected Developer ID or Apple’s own signature, not merely that some process opened a connection to the service’s Mach port. Skipping this validation step is a genuine, real vulnerability class in XPC service design: a service that services requests without verifying the caller’s actual identity is trusting a client’s own self-reported claims about who it is, rather than the code-signing-backed identity XPC makes available specifically to avoid needing to make that trust leap at all.

Synchronous versus asynchronous XPC calls

XPC supports both fire-and-forget asynchronous messages and synchronous, reply-expecting calls that block the caller until the service responds — a design choice each individual XPC method call makes based on whether the caller actually needs a result back before continuing. This flexibility matters in practice because forcing every cross-process call to be either always-synchronous or always-asynchronous would be a poor fit for the genuinely mixed nature of real privilege-separated workloads — a UI-blocking operation (fetching a value the calling code needs immediately to proceed) benefits from a synchronous call’s simplicity, while a fire-and-forget notification (logging an event, triggering a background task) is better served by an asynchronous one that doesn’t stall the caller waiting on a reply it doesn’t actually need.

Related:

Sources:

Comments