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 lookup
  • com.apple.container.xpc.error — Optional JSON-encoded ContainerXPCError for 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.swift creates Mach service listeners using xpc_connection_create_mach_service, validates client identity via audit_token_to_euid, and dispatches to handlers through a route table keyed by XPCMessage.routeKey.
  • Client Architecture: Sources/ContainerXPC/XPCClient.swift establishes connections and sends messages with timeout handling, while XPCClientSession manages persistent state and disconnect detection.
  • Message Protocol: Sources/ContainerXPC/XPCMessage.swift defines dictionary-based messages with the com.apple.container.xpc.route field for routing and com.apple.container.xpc.error for serialized ContainerXPCError payloads.
  • Error Handling: Both sides convert between ContainerizationError and ContainerXPCError to maintain type safety across the XPC boundary.
  • Session Management: Per-client state tracking occurs in XPCServerSession and XPCClientSession, with support for onDisconnect handlers 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →