apple / apple/container

[Bug]: docker exec on a missing executable exits 255, identical to a real exit(255) — no signal exists to tell them apart

Open
#2,210 0 comments 0 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
- [x] If possible, I've reproduced the issue using the 'main' branch of this project

### Steps to reproduce

Via [socktainer](https://github.com/socktainer/socktainer) (a Docker-compatible REST API built directly on `ContainerAPIClient`/`ClientProcess`), but the underlying behavior is in this project's own client API, not socktainer's:

```bash
CID=$(docker run -d --rm alpine sleep 30)

# A genuinely missing executable
docker exec "$CID" nonexistent-binary-xyz
echo $?
# 255

# A real program that deliberately calls exit(255)
docker exec "$CID" sh -c 'exit 255'
echo $?
# 255 — identical
```

Both produce exit code `255` via `ClientProcess.wait() async throws -> Int32`, with no other observable difference: same call sequence, no thrown error from `createProcess`/`start()` in either case, no additional field on the wait result, nothing logged differently between the two.

### Problem description

**Actual behavior:** a failed `exec()` inside the guest (target executable doesn't exist) and a process that legitimately exits with status 255 are indistinguishable through the public API. `ClientProcess.wait()` returns a bare `Int32` in both cases.

**Expected behavior:** real Docker (via runc/containerd) detects the `execve()` `ENOENT` specifically and reports exit code `127` for "command not found" (and `126` for "found but not executable") — a deliberate, long-standing convention many tools depend on. `testcontainers-go`'s wait strategies are one concrete consumer: https://github.com/testcontainers/testcontainers-go/blob/v0.44.0/wait/host_port.go#L240-L244 checks for exit code 127 specifically to detect a missing `nc`/`cat` binary, and gets a false negative here.

I looked for where this distinction might already exist but just isn't surfaced through `ClientProcess`, and couldn't find it:

- `Sources/Services/ContainerAPIService/Client/ClientProcess.swift`'s `wait()` returns a plain `Int32`.
- `Sources/Services/RuntimeLinux/Server/RuntimeService.swift` and `Sources/Services/Runtime/RuntimeClient/RuntimeClient.swift` (the guest-side process creation/exec path) have no `ENOENT`-specific or "executable not found" handling I could find — no distinct error path for this case, and nothing suggesting the raw `execve()` failure is inspected before becoming a generic exit status.

So this looks like it isn't just unsurfaced — the distinguishing information (that the process never actually started because exec itself failed, versus a process that ran and chose to exit 255) may not be captured at all between the guest's exec attempt and what reaches `ClientProcess.wait()`.

**Why this can't be worked around downstream:** a consumer of this API (socktainer, in my case) cannot safely translate exit code 255 to 127 after the fact — the reproduction above shows that would misclassify any real program that legitimately exits with status 255, trading one Docker incompatibility for a different, worse one. Fixing this needs the actual `execve()` failure detected at the point it happens (like runc/containerd do) and surfaced as a distinguishable signal — a different code, a thrown error, or a flag — before it collapses into an ordinary exit status.

### Environment

- OS: macOS 26.6.2 (25G83)
- Container: container CLI version 1.3.0 (build: release, commit: unspeci)
- Reproduced via socktainer built from `main` (commit `1f42b0d`)

Contributor guide

Open the contributing guide

Research direction

Run the Docker reproduction first, then trace ClientProcess.wait() in Sources/Services/ContainerAPIService/Client/ClientProcess.swift through process creation in Sources/Services/RuntimeLinux/Server/RuntimeService.swift and Sources/Services/Runtime/RuntimeClient/RuntimeClient.swift. Determine where an execve ENOENT failure becomes exit status 255 and identify how the public API can preserve that distinction. Done means missing executables are distinguishable from programs that legitimately exit 255 without breaking normal process waits.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker, swift
Domain
api, infrastructure
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.