apple / apple/container

[Bug]: ClientProcess.kill always fails with "missing signal in xpc message" - client sends the signal as an integer, server reads it as a string

Open Beginner friendly
#1,941 2 comments 2 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.