Skip to content
macOSDeep Dive Published Updated 4 min readViews unavailable

macOS XPC Services: Process Boundaries, Launchd, and Safe Message Contracts

Design macOS XPC services with explicit process lifetimes, narrow interfaces, safe decoding, interruption recovery, and practical privilege boundaries.

XPC is macOS’s interprocess communication system for exchanging structured messages across process boundaries. An XPC service is not simply a helper executable that an application launches and supervises itself: the service is packaged as part of an app or framework and launchd manages its on-demand process lifecycle. This separation can isolate crash-prone or sensitive work, but it does not make the service safe by default. The security boundary is only as strong as the service’s interface, code-signing configuration, entitlements, and validation of each request.

Apple exposes lower-level XPC APIs and the object-oriented Foundation API built around NSXPCConnection. The latter presents remote method calls through an interface, but the call still crosses a trust and failure boundary. A client must be prepared for invalidated connections, unavailable replies, and service restarts.

Choose a helper type for its lifecycle

Apple distinguishes app-bundled XPC services, per-user launch agents, and system launch daemons. A standard XPC service is tied to its client relationship and launched on demand; a launch agent runs in a logged-in user’s context, while a launch daemon is system-wide and can run as root. These are not interchangeable deployment formats. Use an XPC service for app-private work that benefits from process separation. Do not turn a system daemon into an XPC service merely to get a remote-call-shaped API, and do not grant root to a helper that only needs a narrow file or network operation.

The distinction matters for user-session resources as well. A system process has no ordinary GUI session. Access to a keychain, pasteboard, or user-specific state depends on the service type and configuration, not on the fact that a request originated from a GUI app. Document which identity and session the helper uses.

Treat the interface as a versioned protocol

An NSXPCInterface defines which methods may cross the connection. Keep that interface small: expose operations such as “validate this document” rather than a general method that accepts arbitrary paths, class names, or serialized objects. For parameters containing Objective-C collection types, configure the allowed classes explicitly. Secure decoding should reject unexpected classes instead of trying to make arbitrary object graphs work across the process boundary.

Validate data again inside the service. A client-side validation is not a security check because the service may have other clients, a corrupted caller, or a future call path that bypasses the UI. Apply size limits, normalize paths according to the operation’s semantics, and authorize access using the service’s own entitlements and the caller identity that Apple makes available through the connection model.

Handle interruption and invalidation as normal states

The service can be terminated and relaunched independently of the client’s process. Install interruption and invalidation handlers, cancel or fail pending work deterministically, and rebuild the connection when the service is expected to return. Avoid synchronous remote calls on the main thread: a stalled service otherwise becomes a frozen user interface. Prefer asynchronous request/reply patterns and define an application-level timeout where the product needs one.

Do not assume that a request ran exactly once when its reply is lost. If a client retries after interruption, an operation such as “append this record” may be duplicated. Design idempotent commands, attach request identifiers where necessary, or provide a query that determines whether the prior operation committed. XPC transports messages; it does not provide an application transaction protocol.

Example interface shape

An interface should express a narrow capability and return a result asynchronously. The following is a conceptual Swift shape; the actual exported protocol, allowed classes, target SDK, and signing setup must be implemented and tested in Xcode:

@objc protocol DocumentCheckService {
    func check(documentURL: URL, reply: @escaping (Bool, String?) -> Void)
}

The service should independently verify that the URL is within an authorized scope and that the file is of an expected type. It should not trust an arbitrary URL simply because the caller is part of the same product. If a security-scoped bookmark or an app-group container is required, make that explicit in the protocol and validate it at the point of use.

Packaging and verification

Bundle an XPC service in the expected application location and configure its bundle identifier and service metadata according to the selected Xcode service type. Sign both the client and service with the intended entitlements. On modern macOS, use Apple’s current XPC and Service Management documentation rather than copying an old launchd plist or helper-installation recipe: Apple’s archived Daemons and Services guide describes historical APIs and compatibility contexts that have since changed.

Test service launch from a clean install, first request, concurrent clients, client exit, service crash, malformed request, and an unavailable reply. Inspect the signed product, not only the source target: the shipped nested bundle and entitlements are the operative security configuration. Finally, test the least-privileged configuration on a non-administrator account. A helper that only works after broadening its sandbox or installing an unnecessary privileged daemon has not yet demonstrated a sound process boundary.

Related:

Sources:

Comments