I have done the following
Steps to reproduce
This race condition can be reliably reproduced by injecting an artificial delay into the C-level libxpc callback while calling XPCClient.send().
-
Check out the main branch.
-
In any XPC test file (e.g., Tests/ContainerXPCTests/XPCClientTests.swift), add a test that configures the mock XPC connection to delay its reply for 3 seconds:
func testXPCClientTimeoutRaceCondition() async throws {
// 1. Setup mock connection to sleep for 3.0s before replying
let client = XPCClient(connection: slowMockConnection)
do {
// 2. Call with a shorter timeout (1.0s)
_ = try await client.send(message, timeout: 1.0)
} catch {
print("Task successfully timed out")
}
// 3. Keep the process alive to wait for the delayed C callback to finally fire
try await Task.sleep(nanoseconds: 3_000_000_000)
// -> FATAL CRASH occurs here: SWIFT TASK CONTINUATION MISUSE
}
-
Run the test: swift test --filter testXPCClientTimeoutRaceCondition
-
Observe that the Swift task correctly times out after 1 second, but 2 seconds later the libxpc callback fires and crashes the entire process.
Problem description
When XPCClient.send() races a Swift timeout task against a libxpc callback, the Swift timeout can cancel the task group. However, because C callbacks cannot be cancelled natively in Swift Concurrency, the delayed XPC callback will eventually fire after the timeout task has completed. It then attempts to resume a CheckedContinuation that was already torn down, causing a fatal SWIFT TASK CONTINUATION MISUSE crash (resuming a continuation more than once).
The correct behavior should be to wrap the CheckedContinuation in a private thread-safe Actor (like ContinuationResolver) so that the continuation can only be resumed exactly once, and late XPC replies are safely swallowed as no-ops instead of crashing the daemon.
Environment
- OS: macOS 26.3.1 (25D2128)
- Xcode: 26.6 (17F113)
- Container: container CLI version 1.1.0
Code of Conduct
I have done the following
Steps to reproduce
This race condition can be reliably reproduced by injecting an artificial delay into the C-level libxpc callback while calling XPCClient.send().
Check out the main branch.
In any XPC test file (e.g., Tests/ContainerXPCTests/XPCClientTests.swift), add a test that configures the mock XPC connection to delay its reply for 3 seconds:
func testXPCClientTimeoutRaceCondition() async throws {
// 1. Setup mock connection to sleep for 3.0s before replying
let client = XPCClient(connection: slowMockConnection)
}
Run the test: swift test --filter testXPCClientTimeoutRaceCondition
Observe that the Swift task correctly times out after 1 second, but 2 seconds later the libxpc callback fires and crashes the entire process.
Problem description
When XPCClient.send() races a Swift timeout task against a libxpc callback, the Swift timeout can cancel the task group. However, because C callbacks cannot be cancelled natively in Swift Concurrency, the delayed XPC callback will eventually fire after the timeout task has completed. It then attempts to resume a CheckedContinuation that was already torn down, causing a fatal SWIFT TASK CONTINUATION MISUSE crash (resuming a continuation more than once).
The correct behavior should be to wrap the CheckedContinuation in a private thread-safe Actor (like ContinuationResolver) so that the continuation can only be resumed exactly once, and late XPC replies are safely swallowed as no-ops instead of crashing the daemon.
Environment
Code of Conduct