Skip to content

Run tini as PID 1 in sandbox containers, so orphans get reaped - #45

Open
tadamcz wants to merge 3 commits into
mainfrom
tini-pid1
Open

Run tini as PID 1 in sandbox containers, so orphans get reaped#45
tadamcz wants to merge 3 commits into
mainfrom
tini-pid1

Conversation

@tadamcz

@tadamcz tadamcz commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Ports epoch-research/PortBench#676 to this repo.

On the k8s backend, PID 1 in every sandbox pod is the agent-env chart's default command, tail -f /dev/null, which never calls wait(). Anything a sandbox exec orphans is reparented to PID 1 and stays there as a zombie holding its process slot for the life of the pod; once enough accumulate, fork() returns EAGAIN and every later command in the sample fails, permanently. PortBench measured this killing ~6% of samples across three eval sets, and diagnosed/verified the fix on a live pod (see the PR above and the upstream issue).

Local Docker never shows it because the compose init: true is honoured there (docker-init reaps). Kubernetes has no init equivalent, so the fix has to be the pod command itself.

The port differs from PortBench in shape because this repo writes the k8s config natively in chart vocabulary instead of letting hawk convert the compose file:

  • Both backend writers now take their main command from a shared SANDBOX_COMMAND constant (/usr/bin/tini -s -- tail -f /dev/null), keeping the two artifacts from drifting. -s makes tini a subreaper wherever something else is already PID 1 (the local-docker case), so the one command is correct on both backends.
  • tini lands in the Dockerfile's base stage, not a service stage: all four service images (agent, agent_corpus, comparator, generate) build FROM base and the command is shared by both services, so an image lacking tini would fail to start at all. It is folded into base's existing apt line.
  • The code comment records this as a workaround and links the upstream issue, so it can be deleted if inspect_k8s_sandbox#251 is fixed.

Relevant note: this invalidates the base layer, so every image rebuilds from scratch. ECR tags are immutable and keyed on apn.__version__, so the PR includes a bump to 0.1.10 — without it the cluster would keep running the pre-tini images.

On the k8s backend PID 1 in every sandbox pod is the agent-env chart's
default command, `tail -f /dev/null`, which never calls wait(). Each
sandbox exec runs in a fresh process tree, so anything it orphans is
reparented to PID 1 and stays there as a zombie, holding its process
slot for the life of the pod. Once enough slots are held, fork()
returns EAGAIN and every subsequent command in the sample fails,
permanently. PortBench hit exactly this in production
(epoch-research/PortBench#676); roughly 6% of samples across three
eval sets died to it there.

Local Docker never shows it: the compose file's `init: true` puts
docker-init above tail, and docker-init reaps. Kubernetes has no init
equivalent (kubernetes/kubernetes#84210), and our values file is
written in chart vocabulary directly, so there the fix has to be the
command itself: `/usr/bin/tini -s -- tail -f /dev/null`. `-s` makes
tini a subreaper where something else is already PID 1, so the same
command is correct on both backends -- both writers now draw it from
the shared SANDBOX_COMMAND constant, per that block's no-drift rule.

tini is installed in the Dockerfile's base stage rather than a service
stage: every service image (agent, agent_corpus, comparator, generate)
builds FROM base, and the command is shared by both services in each
backend's config, so a service whose image lacked tini would fail to
start at all. It is folded into base's existing apt line.

This is a workaround; remove it if the upstream issue is fixed:
UKGovernmentBEIS/inspect_k8s_sandbox#251

Note this invalidates the base layer, so every image rebuilds from
scratch, and ECR tags are immutable and keyed on apn.__version__, so
the cluster only picks the fix up at the next version bump.
Both places that link inspect_k8s_sandbox#251 now carry an explicit
TODO to remove the tini workaround if it is fixed upstream.
ECR tags are immutable and keyed on apn.__version__, so without a bump
the cluster would keep running the pre-tini images.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant