I have done the following
Steps to reproduce
The failure is in the client library (ContainerAPIClient), so the reproduction is a small Swift program rather than a CLI command.
- Start the engine and create a throwaway container to exec into:
container system start
container run --detach --name kill-repro alpine sleep 600
- Build and run this program in a package that depends on
github.com/apple/container (product ContainerAPIClient):
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)")
}
- 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.
- 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):
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):
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
I have done the following
Steps to reproduce
The failure is in the client library (ContainerAPIClient), so the reproduction is a small Swift program rather than a CLI command.
github.com/apple/container(productContainerAPIClient):killthrows and the sleep keeps running:The same happens with every signal value, not only SIGKILL.
container delete --force kill-reproProblem description
ClientProcess.kill(_: Int32)never delivers a signal. Every call fails withinvalidArgument: "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):but the server's handler for the
.containerKillroute reads that key as a string, and rejects the request when the string is absent (Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift, thesignal()helper):The container-level kill shows the intended contract:
ContainerClient.kill(id:signal:)takes the signal as aString, writes it as a string, and works.ClientProcess.killis 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 oncreateProcesscan stop waiting for a command, but cannot terminate it; the command keeps running until the container stops. The failure is also easy to miss, becausekillis the kind of call that often gets wrapped intry?.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
Code of Conduct