Skip to content

fix: reply to unknown XPC routes and make request timeout cancellable#1862

Open
radheradhe01 wants to merge 1 commit into
apple:mainfrom
radheradhe01:fix/xpc-unknown-route-and-timeout
Open

fix: reply to unknown XPC routes and make request timeout cancellable#1862
radheradhe01 wants to merge 1 commit into
apple:mainfrom
radheradhe01:fix/xpc-unknown-route-and-timeout

Conversation

@radheradhe01

@radheradhe01 radheradhe01 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

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.

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 radheradhe01 changed the title XPC: reply to unknown routes and make the request timeout actually cancel the waiter fix: reply to unknown XPC routes and make request timeout cancellable Jun 30, 2026
@stephenlclarke

Copy link
Copy Markdown

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.

stephenlclarke pushed a commit to stephenlclarke/container that referenced this pull request Jul 15, 2026
Import apple#1862 unchanged on top of current Apple main, preserving the original author and implementation.
stephenlclarke added a commit to stephenlclarke/container that referenced this pull request Jul 15, 2026
Exercise the timeout, caller-cancellation, late-reply, client-reuse, and unknown-route paths introduced by apple#1862.
stephenlclarke added a commit to stephenlclarke/container that referenced this pull request Jul 15, 2026
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.
@JeronimoColon

Copy link
Copy Markdown

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants