apple / apple/container

[Bug]: Container log file stops receiving output permanently when an attached client dies (MultiWriter aborts fan-out on first EPIPE)

Open
#2,009 3 comments 0 reactions 0 assignees View on GitHub
stability
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

1. Run a container in the foreground (attached stdio) that logs periodically:
```sh
container run --name freeze alpine:latest sh -c 'while true; do echo "tick $(date -u +%H:%M:%S)"; sleep 2; done'
```
2. From another terminal, kill the attached CLI process without giving it a chance to clean up (simulates a client crash, terminal close, SSH drop, or supervisor restart of any program holding the attach):
```sh
kill -9
```
3. Confirm the container is still running, then check its logs repeatedly:
```sh
container ls # freeze ... running
container logs freeze # last line is from the moment of the kill
sleep 10
container logs freeze # identical output — nothing new, ever
```

Observed (timestamps from a real run): CLI killed at `21:41:21` UTC → `container logs` frozen at `tick 21:41:22` permanently, while the container keeps running and emitting every 2 seconds. The bundle's `stdio.log` on disk stops growing at that moment, so this is the write path, not `container logs`' read path.

### Problem description

`RuntimeService` wires an attached container's stdout/stderr through `MultiWriter(handles: [clientStdioHandle, containerLogHandle])` (Sources/Services/RuntimeLinux/Server/RuntimeService.swift), and `MultiWriter.write` aborts the whole fan-out on the first failing handle:

```swift
func write(_ data: Data) throws {
for handle in handles {
try handle.write(contentsOf: data)
}
}
```

The client's stdio handle comes **first**. Once the attached client dies, every write to that handle fails (EPIPE), the loop throws before reaching the log file handle, and `stdio.log` never receives another byte for the container's remaining lifetime — silently, with the container otherwise healthy.

Real-world impact: any long-running container started attached loses all logging from the moment its original client goes away for any reason. We hit this in production-like use through a Docker API bridge over `container` (socktainer/Whalebridge): a PostgreSQL container's logs froze mid-day and `docker logs` served the same stale buffer for hours; the trigger was the bridge daemon being restarted while the container ran on. The repro above shows the same failure with nothing but the `container` CLI itself.

Expected behavior: the death of one output consumer should not disable the others. `MultiWriter.write` should isolate per-handle failures — e.g. attempt every handle and swallow (or collect) individual errors, and ideally drop a handle after a persistent failure so the container's own log file keeps working:

```swift
func write(_ data: Data) throws {
for handle in handles {
do { try handle.write(contentsOf: data) } catch { /* drop or note dead handle */ }
}
}
```

(Ordering the log file first would narrow the window but not fix it; per-handle isolation does.)

Happy to submit a PR with the fix plus a regression test if that's welcome.

### Environment

- OS: macOS 27.0 (Apple silicon)
- Xcode: Xcode 26
- Container: container CLI version 1.1.0 (build: release, commit: 5973b9c)

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Contributor guide

Open the contributing guide

Research direction

Start in Sources/Services/RuntimeLinux/Server/RuntimeService.swift and inspect how MultiWriter connects the client stdio and container log handles. Reproduce the failure with the attached container command and kill -9 scenario, then verify that writes continue reaching stdio.log after the client handle fails. Add a regression test if the surrounding test structure supports it.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.