[Bug]: ClientProcess.kill always fails with "missing signal in xpc message" - client sends the signal as an integer, server reads it as a string
- Dominant language
- Swift
- Stars
- 49.9k
- Forks
- 1.8k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 22
Description
### I have done the following
- [x] I have searched the existing issues
- [ ] If possible, I've reproduced the issue using the 'main' branch of this project
### Steps to reproduce
The failure is in the client library (ContainerAPIClient), so the reproduction is a small Swift program rather than a CLI command.
1. Start the engine and create a throwaway container to exec into:
```sh
container system start
container run --detach --name kill-repro alpine sleep 600
```
2. Build and run this program in a package that depends on `github.com/apple/container` (product `ContainerAPIClient`):
```swift
import ContainerAPIClient
import Foundation
let client = ContainerClient()
let container = try await client.get(id: "kill-repro")
// Start an exec'd process (a long sleep) so there is a live process to signal.
var processConfig = container.configuration.initProcess
processConfig.executable = "/bin/sleep"
processConfig.arguments = ["300"]
processConfig.terminal = false
let process = try await client.createProcess(
containerId: container.id,
processId: UUID().uuidString.lowercased(),
configuration: processConfig,
stdio: [nil, nil, nil]
)
try await process.start()
do {
try await process.kill(SIGKILL)
print("kill succeeded")
} catch {
print("kill failed: \(error)")
}
```
3. Observe that `kill` throws and the sleep keeps running:
```
kill failed: invalidArgument: "missing signal in xpc message"
```
The same happens with every signal value, not only SIGKILL.
4. Clean up: `container delete --force kill-repro`
### Problem description
`ClientProcess.kill(_: Int32)` never delivers a signal. Every call fails with `invalidArgument: "missing signal in xpc message"`.
The cause is a type mismatch in the XPC message between the client and the API server. The client writes the signal as an integer (`Sources/Services/ContainerAPIService/Client/ClientProcess.swift`):
```swift
request.set(key: .signal, value: Int64(signal))
```
but the server's handler for the `.containerKill` route reads that key as a string, and rejects the request when the string is absent (`Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift`, the `signal()` helper):
```swift
guard let signal = self.string(key: .signal) else {
throw ContainerizationError(.invalidArgument, message: "missing signal in xpc message")
}
```
The container-level kill shows the intended contract: `ContainerClient.kill(id:signal:)` takes the signal as a `String`, writes it as a string, and works. `ClientProcess.kill` is the odd one out. Since it is also the only API that targets a specific process, there is currently no working way to signal an exec'd process through the client library at all. In practice that means a timeout or cancel feature built on `createProcess` can stop waiting for a command, but cannot terminate it; the command keeps running until the container stops. The failure is also easy to miss, because `kill` is the kind of call that often gets wrapped in `try?`.
What I would expect: the client and server agree on the wire type, and the signal is delivered.
I observed the failure at runtime on container 1.0.0. The client write and the server read are both unchanged at tag 1.1.0 and on current main (checked at 5f277a9), so I expect it reproduces there as well.
Related work I found while searching: #1747 reports this same root cause surfacing through terminal-resize signal forwarding, and #1778 fixes the client-side encoding. The approach in #1778 (send the signal name rather than a number) looks right to me: Darwin and Linux disagree on some signal numbers (USR1 is 30 on Darwin and 10 on Linux), so a numeric passthrough could deliver the wrong signal even if the server accepted numbers.
### Environment
- OS: macOS 26.5.2 (25F84)
- Xcode: 26.6 (17F113)
- Container: container CLI 1.0.0 (build: release, commit: ee848e3)
### Code of Conduct
- [x] I agree to follow this project's Code of Conduct
Contributor guide
Research direction
Start with Sources/Services/ContainerAPIService/Client/ClientProcess.swift and the signal() helper in Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift. Compare the process-kill encoding with the working container-level kill and the approach in #1778. Done means ClientProcess.kill delivers the requested signal without the missing-signal error, including signals whose numeric values differ across platforms.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100