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.
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_mbfrom inside a sandbox. The memory cap works — the container is OOM-killed,Exited (137)— which is the correct outcome. Then exec into it:Meanwhile the daemon's own log has the real cause:
And the state is already exposed on another endpoint:
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"}409or410both read reasonably;500is the one thing it should not be, because it asserts a server fault where there is none. The existingkindfield 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_mbshould 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_mbfor capacity planning should assume meaningful headroom above the configured figure rather than treating it as a hard bound.