Skip to content

A stopped sandbox returns an opaque 500, so callers cannot distinguish it from a daemon fault #29

Description

@Lutherwaves

Exec against a sandbox that has stopped returns an opaque 500 {"error":"internal error","kind":"internal"}. A caller cannot tell "your sandbox is gone" from "the daemon is broken", even though the daemon knows exactly which it is.

Reproduce

Exhaust a profile's memory_mb from inside a sandbox. The memory cap works — the container is OOM-killed, Exited (137) — which is the correct outcome. Then exec into it:

$ curl --unix-socket … -X POST …/sandboxes/<name>/exec -d '{"argv":["echo","hi"]}'
{"error":"internal error","kind":"internal"}
HTTP 500

Meanwhile the daemon's own log has the real cause:

ERROR openbloxd request failed error="exec create in \"<name>\": Error response from daemon:
container <id> is not running"

And the state is already exposed on another endpoint:

$ curl --unix-socket … …/sandboxes/<name>
{"name":"<name>", …, "state":"stopped", …}
HTTP 200

So the information exists, is accurate, and is reachable — just not on the path where a caller trips over the condition.

Why this matters more than a cosmetic message

Redacting internals behind a generic error is the right default, and I am not proposing that change. But a stopped sandbox is not an internal fault — it is a predictable, caller-actionable state with a legitimate cause. Hitting the memory ceiling is ordinary behaviour for untrusted code, which is precisely what the ceiling is for.

Because the condition is indistinguishable from a daemon fault, a caller cannot implement the obvious recovery — notice the sandbox is gone, create a fresh one, tell the user why their state vanished. It has to either treat every 500 as possibly-recoverable and retry blindly, or poll GET /sandboxes/{name} after each failure to disambiguate.

Suggestion

Give the condition its own kind on the exec (and file/process) paths, so it is distinguishable without a second round trip:

{"error":"sandbox is stopped","kind":"stopped"}

409 or 410 both read reasonably; 500 is the one thing it should not be, because it asserts a server fault where there is none. The existing kind field already carries this sort of distinction, so nothing structural has to change.

Worth applying to any endpoint that requires a running sandbox, not just exec — the same flattening will be there.

Adjacent observation, not the ask

The kill lands on the whole sandbox rather than the offending process, so an OOM destroys the session and everything in it. That may well be inherent to how the limit is applied, and it is arguably the safe behaviour. But it does mean OOM is a session-ending event rather than a failed command, which is worth being explicit about in the docs — a caller sizing memory_mb should know that hitting it costs the user everything in that sandbox, not just the one call.

Separately: a process reached roughly 2.8 GiB resident inside a profile configured for 2048 MiB before the kill landed. The ceiling holds, but not tightly — anyone using memory_mb for capacity planning should assume meaningful headroom above the configured figure rather than treating it as a hard bound.

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