Skip to content

[Bug]: Force-unwrapped xpc_dictionary_create_reply crashes any XPC server on fire-and-forget input #2281

Description

@1PoPTRoN

I have done the following

  • I have searched the existing issues
  • If possible, I've reproduced the issue using the 'main' branch of this project

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 agree to follow this project's Code of Conduct

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions