Skip to content

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

Description

@JustMaris

I have done the following

  • I have searched the existing issues
  • If possible, I've reproduced the issue using the 'main' branch of this project

Steps to reproduce

Via 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:

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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions