From 2999168307d76c30f9f6a11a52834eb892991cfb Mon Sep 17 00:00:00 2001 From: Bhavesh Varma <151822885+radheradhe01@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:49:05 +0530 Subject: [PATCH] XPC: reply to unknown routes and make the request timeout actually cancel the waiter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 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. --- Sources/ContainerXPC/XPCClient.swift | 18 ++++--- Sources/ContainerXPC/XPCReplyBox.swift | 68 ++++++++++++++++++++++++++ Sources/ContainerXPC/XPCServer.swift | 10 ++++ 3 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 Sources/ContainerXPC/XPCReplyBox.swift diff --git a/Sources/ContainerXPC/XPCClient.swift b/Sources/ContainerXPC/XPCClient.swift index bab008509..ea4edb7e5 100644 --- a/Sources/ContainerXPC/XPCClient.swift +++ b/Sources/ContainerXPC/XPCClient.swift @@ -111,15 +111,19 @@ extension XPCClient { } group.addTask { - try await withCheckedThrowingContinuation { cont in - xpc_connection_send_message_with_reply(self.connection, message.underlying, nil) { reply in - do { - let message = try self.parseReply(reply) - cont.resume(returning: message) - } catch { - cont.resume(throwing: error) + let box = XPCReplyBox() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { cont in + box.store(cont) + xpc_connection_send_message_with_reply(self.connection, message.underlying, nil) { reply in + box.resume { try self.parseReply(reply) } } } + } onCancel: { + // On timeout the group cancels this task. The XPC reply + // handler is not cancellation-aware, so resume the + // continuation here; a late reply becomes a no-op. + box.resume { throw CancellationError() } } } diff --git a/Sources/ContainerXPC/XPCReplyBox.swift b/Sources/ContainerXPC/XPCReplyBox.swift new file mode 100644 index 000000000..24a9a1c07 --- /dev/null +++ b/Sources/ContainerXPC/XPCReplyBox.swift @@ -0,0 +1,68 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the container project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +#if os(macOS) +import Foundation + +/// Resume-once bridge between a `CheckedContinuation` and the XPC reply handler. +/// +/// The XPC reply handler is not cancellation-aware, so a request can outlive a +/// client-side timeout. This box lets either the reply handler or the task's +/// cancellation handler resume the continuation, whichever fires first, while +/// guaranteeing it is resumed exactly once. A continuation resumed twice traps. +final class XPCReplyBox: @unchecked Sendable { + private let lock = NSLock() + private var cont: CheckedContinuation? + private var resumed = false + private var pending: Result? + + /// Store the continuation. If a resume already raced ahead (cancellation + /// before the continuation was installed), honor it immediately. + func store(_ cont: CheckedContinuation) { + lock.lock() + if let pending, !resumed { + resumed = true + lock.unlock() + cont.resume(with: pending) + return + } + self.cont = cont + lock.unlock() + } + + /// Resume the continuation with the result of `body`, at most once. A later + /// call is a no-op, so the reply handler and the cancellation handler can + /// both call it safely. + func resume(_ body: () throws -> XPCMessage) { + let result = Result { try body() } + lock.lock() + guard !resumed else { + lock.unlock() + return + } + guard let cont else { + pending = result + lock.unlock() + return + } + resumed = true + self.cont = nil + lock.unlock() + cont.resume(with: result) + } +} + +#endif diff --git a/Sources/ContainerXPC/XPCServer.swift b/Sources/ContainerXPC/XPCServer.swift index b33453142..a46dc0873 100644 --- a/Sources/ContainerXPC/XPCServer.swift +++ b/Sources/ContainerXPC/XPCServer.swift @@ -241,6 +241,16 @@ public struct XPCServer: Sendable { } xpc_connection_send_message(connection, reply.underlying) } + } else { + // No handler for this route: reply with an error instead of dropping + // the message, otherwise the client blocks until its timeout (or + // forever, if it sent none). + log.error("no handler registered for route", metadata: ["route": "\(route)"]) + Self.replyWithError( + connection: connection, + object: object, + err: ContainerizationError(.invalidArgument, message: "unknown route: \(route)") + ) } }