You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Unknown route hangs the client.XPCServer dispatches with if let handler = routes[route] { ... } and no else. An unknown route produces no reply at all, so the client blocks until its timeout (or forever, if it sent none).
The client timeout is illusory.XPCClient.send races a timeout task against the reply task in a withThrowingTaskGroup. The reply task uses withCheckedThrowingContinuation, which is not cancellation-aware. When the timeout fires, the group tears down and awaits the reply task — which only completes when the daemon actually replies. Against a live-but-hung daemon the "timeout" never returns.
This change:
XPCServer: adds the else branch and replies with an invalidArgument error for unknown routes.
XPCClient: wraps the reply task in withTaskCancellationHandler and bridges the continuation through a small resume-once box (XPCReplyBox). On cancellation the continuation is resumed promptly; a late XPC reply becomes a no-op. The box guarantees the continuation is resumed exactly once. The connection is left intact on timeout; only the pending request is abandoned.
Testing
Tested locally
Added/updated tests
Added/updated docs
Verified by static / code-level review; not built locally (no macOS 26 toolchain available here) — CI build will validate.
…ncel the waiter
### Problem
Two related issues in the XPC request path:
1. **Unknown route hangs the client.** `XPCServer` dispatches with `if let handler = routes[route] { ... }` and **no `else`**. An unknown route produces no reply at all, so the client blocks until its timeout (or forever, if it sent none).
2. **The client timeout is illusory.** `XPCClient.send` races a timeout task against the reply task in a `withThrowingTaskGroup`. The reply task uses `withCheckedThrowingContinuation`, which is **not** cancellation-aware. When the timeout fires, the group tears down and awaits the reply task — which only completes when the daemon actually replies. Against a live-but-hung daemon the "timeout" never returns.
### Fix
- `XPCServer`: add the `else` branch and reply with an `invalidArgument` error for unknown routes.
- `XPCClient`: wrap the reply task in `withTaskCancellationHandler` and bridge the continuation through a small resume-once box (`XPCReplyBox`). On cancellation the continuation is resumed promptly; a late XPC reply becomes a no-op. The box guarantees the continuation is resumed exactly once.
### Notes
The connection is left intact on timeout; only the pending request is abandoned.
radheradhe01
changed the title
XPC: reply to unknown routes and make the request timeout actually cancel the waiterfix: reply to unknown XPC routes and make request timeout cancellableJun 30, 2026
I validated this implementation on current Apple main as the first standalone commit of #1935, preserving the original author and code unchanged. The follow-up commit adds deterministic coverage for timeout return, caller cancellation, late replies, client reuse after timeout, cancellation before continuation storage, and unknown-route replies. If preferred, those tests can be folded into this PR; #1935's separate functional delta is the shorter root-help plugin-discovery policy.
Use a one-second API health deadline for container --help, container help, and bare container while preserving the normal ten-second plugin dispatch deadline. Fall back to built-in help when the daemon is unavailable.
Depends on the cancellable XPC request handling from apple#1862 and supersedes apple#1838 for apple#1459.
I can confirm the illusory-timeout half of this from an independent reproduction against a stock 1.1.0 install, hit while building on ContainerAPIClient.
ContainerClient.copyOut sends its request with a 300 second responseTimeout. Point it at a guest path that does not exist (the #1927 stall) and the call does not return at 300 seconds. In my capture it sat for about sixteen minutes, and it returned only when I killed the container's runtime helper so the daemon could finally send back an error. The error that then surfaced was the timeout's own "XPC timeout for request to com.apple.container.apiserver/containerCopyOut" - selected at the 300 second mark, then stuck behind the wait on the non-cancellable reply continuation, exactly as this PR describes. The diagnosis matches what the wire actually does.
Two notes from reading the change:
The resume-once handoff checks out, including the racy corner: onCancel can fire before store installs the continuation, and the pending stash covers that ordering, so neither side can double-resume or drop a resume. The timeout path also surfaces the intended timeout error rather than the reply task's CancellationError, since group.next() selects the timeout first and the cancellation error is discarded in the group teardown.
Consider a small unit test on XPCReplyBox itself: store-then-resume, resume-then-store, and a second resume as a no-op. The resume-once invariant is what stands between this fix and a trapped double resume, and it is testable without a live XPC connection.
Worth cross-linking #1927: with this change that cp fails at its 300 second budget with a clear timeout error instead of hanging until reboot, independent of the guest-side fix in apple/containerization#799. I built the same shape of fix (a resume-once race around the non-cancellable send) into my own client layer and it has held up. Good to see it proposed upstream.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type of Change
Motivation and Context
Two related issues in the XPC request path:
XPCServerdispatches withif let handler = routes[route] { ... }and noelse. An unknown route produces no reply at all, so the client blocks until its timeout (or forever, if it sent none).XPCClient.sendraces a timeout task against the reply task in awithThrowingTaskGroup. The reply task useswithCheckedThrowingContinuation, which is not cancellation-aware. When the timeout fires, the group tears down and awaits the reply task — which only completes when the daemon actually replies. Against a live-but-hung daemon the "timeout" never returns.This change:
XPCServer: adds theelsebranch and replies with aninvalidArgumenterror for unknown routes.XPCClient: wraps the reply task inwithTaskCancellationHandlerand bridges the continuation through a small resume-once box (XPCReplyBox). On cancellation the continuation is resumed promptly; a late XPC reply becomes a no-op. The box guarantees the continuation is resumed exactly once. The connection is left intact on timeout; only the pending request is abandoned.Testing
Verified by static / code-level review; not built locally (no macOS 26 toolchain available here) — CI build will validate.