I have done the following
Steps to reproduce
Reproduced on main @ 57f0b93 (post-1.4.1).
Open an anonymous XPC connection pair, deliver one dictionary fire-and-forget (no reply expected), and call reply() on the received message — this mirrors what every route handler and every XPCServer.handleMessage error path does via message.reply():
let listener = xpc_connection_create(nil, queue)
// … accept peer, capture received dictionary …
let client = xpc_connection_create_from_endpoint(xpc_endpoint_create(listener))
xpc_connection_send_message(client, xpc_dictionary_create_empty()) // fire-and-forget
// server side, on the RECEIVED dictionary:
XPCMessage(object: received).reply() // 💥 traps here
Observed result:
ContainerXPC/XPCMessage.swift:60: Fatal error: Unexpectedly found nil while unwrapping an Optional value
(exit 133 / SIGTRAP — the process is gone, not just the request.)
Problem description
XPCMessage.reply() force-unwraps xpc_dictionary_create_reply(object), which returns NULL for any message sent via xpc_connection_send_message (no reply context). Every error path in XPCServer.handleMessage — not-a-dictionary, uid mismatch, empty route, handler errors — funnels through replyWithError → reply(), and every success path builds its response with reply() too (88 call sites across 13 files, covering all *Harness types and RuntimeService).
So a single one-way dictionary delivered to com.apple.container.apiserver — reachable from any same-uid process through the same per-user bootstrap namespace the daemons themselves use — traps the daemon. The same applies to each helper (container-runtime-linux, container-network-vmnet, container-core-images, machine-apiserver), which share the ContainerXPC code.
What should happen instead: a message with no reply context should be dropped after logging, never trapped on. reply() itself should not be able to crash the process. Note this isn't only a hostile-input concern: the repo already sends fire-and-forget messages in normal operation (ProgressUpdateService uses xpc_connection_send_message), so "no reply context" is a legitimate case the server side must tolerate.
Suggested direction: make reply() non-trapping (fall back to a fresh dictionary), add a canReply check, and gate handleMessage/replyWithError on it.
Happy to put up a PR — I have the fix and regression tests (including the live connection-pair repro above) ready against main.
Environment
- OS: macOS 27.0 (26A428)
- Xcode: CommandLineTools Swift 6.4 (no full Xcode installed)
- Container: main @
57f0b93
Code of Conduct
I have done the following
Steps to reproduce
Reproduced on
main@57f0b93(post-1.4.1).Open an anonymous XPC connection pair, deliver one dictionary fire-and-forget (no reply expected), and call
reply()on the received message — this mirrors what every route handler and everyXPCServer.handleMessageerror path does viamessage.reply():Observed result:
(exit 133 / SIGTRAP — the process is gone, not just the request.)
Problem description
XPCMessage.reply()force-unwrapsxpc_dictionary_create_reply(object), which returnsNULLfor any message sent viaxpc_connection_send_message(no reply context). Every error path inXPCServer.handleMessage— not-a-dictionary, uid mismatch, empty route, handler errors — funnels throughreplyWithError→reply(), and every success path builds its response withreply()too (88 call sites across 13 files, covering all*Harnesstypes andRuntimeService).So a single one-way dictionary delivered to
com.apple.container.apiserver— reachable from any same-uid process through the same per-user bootstrap namespace the daemons themselves use — traps the daemon. The same applies to each helper (container-runtime-linux,container-network-vmnet,container-core-images,machine-apiserver), which share theContainerXPCcode.What should happen instead: a message with no reply context should be dropped after logging, never trapped on.
reply()itself should not be able to crash the process. Note this isn't only a hostile-input concern: the repo already sends fire-and-forget messages in normal operation (ProgressUpdateServiceusesxpc_connection_send_message), so "no reply context" is a legitimate case the server side must tolerate.Suggested direction: make
reply()non-trapping (fall back to a fresh dictionary), add acanReplycheck, and gatehandleMessage/replyWithErroron it.Happy to put up a PR — I have the fix and regression tests (including the live connection-pair repro above) ready against
main.Environment
57f0b93Code of Conduct