How XPC Communication Works Between Container Services in apple/container
The apple/container framework implements a custom RPC layer on top of macOS XPC, using dictionary-based messages routed between XPCServer and XPCClient components with built-in authentication and error handling.
The apple/container repository provides a containerization framework for macOS that relies on secure inter-process communication to coordinate services. Understanding how XPC communication between container services functions requires examining the lightweight RPC implementation built atop Mach service bindings. The architecture separates concerns into distinct server-side listeners and client-side connection managers, with shared message types handling payload serialization and routing across process boundaries.
Server-Side Architecture (XPCServer)
The server implementation in Sources/ContainerXPC/XPCServer.swift establishes a Mach service listener and manages the lifecycle of incoming client connections through a routing table.
Listener Creation and Mach Service Setup
The XPCServer class initializes a Mach service using xpc_connection_create_mach_service to create a named endpoint that clients can discover. This listener runs asynchronously, yielding new connection objects as clients connect to the service identifier.
Connection Acceptance and Session Management
Each incoming xpc_connection_t spawns a new XPCServerSession (defined in Sources/ContainerXPC/XPCServerSession.swift). This session tracks per-client state and manages disconnect handlers registered via session.onDisconnect(_:). The session wraps the raw XPC connection in an AsyncStream<xpc_object_t> to integrate with Swift concurrency.
Message Routing and Handler Dispatch
Incoming messages are interpreted as dictionaries. The server extracts the route key (XPCMessage.routeKey) and performs a lookup in the route table ([String: RouteHandler]). Each registered handler receives the XPCMessage object and returns a reply dictionary that the server sends back to the client.
Security Validation via Audit Tokens
Before processing requests, the server validates the client's effective UID using audit_token_to_euid to ensure it matches the server's UID. This check prevents privilege escalation attacks where a malicious client might attempt to access container services from a different security context.
Error Serialization and Disconnect Handling
If a route handler throws a ContainerizationError, the server serializes it into a ContainerXPCError structure and includes it in the reply under the com.apple.container.xpc.error key. Generic errors receive similar wrapping. When connections terminate, XPCServerSession.fireDisconnect() invokes all registered cleanup handlers.
Client-Side Architecture (XPCClient)
The client implementation in Sources/ContainerXPC/XPCClient.swift handles connection establishment, request marshaling, and response parsing.
Connection Setup and Mach Service Creation
XPCClient creates a connection to the server's Mach service using xpc_connection_create_mach_service. This establishes the underlying transport channel that persists across multiple requests.
Request Sending and Timeout Handling
The send(_:responseTimeout:) method marshals an XPCMessage into an XPC dictionary and transmits it using xpc_connection_send_message_with_reply. The implementation sets up a task group that waits for either the server reply or a timeout, converting XPC-level failures into ContainerizationError instances via the parseReply helper.
Persistent Sessions and Disconnect Monitoring
Clients call openSession() to obtain an XPCClientSession (from Sources/ContainerXPC/XPCClientSession.swift). This session installs a disconnect handler immediately, ensuring that server crashes or unexpected terminations are never missed. Additional cleanup handlers may be registered via session.onDisconnect(_:).
Message Format and Protocol (XPCMessage)
The XPCMessage type in Sources/ContainerXPC/XPCMessage.swift defines the wire protocol used by both ends of the connection.
Dictionary Structure and Routing
Messages are XPC dictionaries containing mandatory fields:
com.apple.container.xpc.route— The RPC endpoint name used for handler lookupcom.apple.container.xpc.error— Optional JSON-encodedContainerXPCErrorfor failure responses
Typed Accessors and Error Propagation
The class provides typed accessors for extracting data, strings, booleans, integers, dates, file handles, and XPC endpoints from the dictionary. Errors serialize as ContainerXPCError (containing code and message fields) and deserialize back into ContainerizationError on the receiving side, preserving type information across the process boundary.
Implementation Example
Creating a Service with Route Handlers
import Logging
import ContainerXPC
let log = Logger(label: "com.apple.container.example")
let routes: [String: XPCServer.RouteHandler] = [
"ping": XPCServer.route { msg in
var reply = msg.reply()
reply.set(key: "result", value: "pong")
return reply
},
"status": XPCServer.route { msg in
// Gather container status...
var reply = msg.reply()
reply.set(key: "running", value: true)
return reply
}
]
let server = XPCServer(identifier: "com.apple.container.example",
routes: routes,
log: log)
try await server.listen()
Sending Requests from a Client
import ContainerXPC
let client = XPCClient(service: "com.apple.container.example")
let session = client.openSession()
var request = XPCMessage(route: "ping")
let response = try await session.send(request)
if let result = response.string(key: "result") {
print("Server replied: \(result)") // → "Server replied: pong"
}
Summary
- Server Architecture:
Sources/ContainerXPC/XPCServer.swiftcreates Mach service listeners usingxpc_connection_create_mach_service, validates client identity viaaudit_token_to_euid, and dispatches to handlers through a route table keyed byXPCMessage.routeKey. - Client Architecture:
Sources/ContainerXPC/XPCClient.swiftestablishes connections and sends messages with timeout handling, whileXPCClientSessionmanages persistent state and disconnect detection. - Message Protocol:
Sources/ContainerXPC/XPCMessage.swiftdefines dictionary-based messages with thecom.apple.container.xpc.routefield for routing andcom.apple.container.xpc.errorfor serializedContainerXPCErrorpayloads. - Error Handling: Both sides convert between
ContainerizationErrorandContainerXPCErrorto maintain type safety across the XPC boundary. - Session Management: Per-client state tracking occurs in
XPCServerSessionandXPCClientSession, with support foronDisconnecthandlers on both ends.
Frequently Asked Questions
How does the XPCServer validate client identity?
The server extracts the audit token from the incoming XPC connection and calls audit_token_to_euid to obtain the client's effective UID. According to the implementation in Sources/ContainerXPC/XPCServer.swift, the server compares this UID against its own process UID and rejects connections that do not match, preventing privilege escalation between different user contexts.
What happens when a container service crashes or disconnects?
Both client and server implement disconnect handling through session objects. On the client side, XPCClientSession (in Sources/ContainerXPC/XPCClientSession.swift) installs a disconnect handler immediately upon creation, ensuring that server crashes trigger cleanup callbacks. The server uses XPCServerSession.fireDisconnect() to notify registered handlers when a client connection terminates.
How are errors propagated between client and server?
Errors thrown by route handlers are caught by the server and serialized into ContainerXPCError structures, which are encoded as JSON and placed in the com.apple.container.xpc.error field of the reply dictionary. The client's parseReply method deserializes these back into ContainerizationError instances, preserving error codes and messages across the process boundary.
Why does apple/container use a custom RPC layer instead of standard XPC?
The framework implements a custom RPC layer to provide type-safe message routing, container-specific error serialization, and structured session management. While built atop xpc_connection_create_mach_service, the abstraction adds mandatory UID validation, timeout handling for requests, and clean integration with Swift concurrency through AsyncStream, which raw XPC APIs do not provide directly.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →