Repository navigation
feat: per-sandbox cgroup v2 CPU/memory/pids limits with level-tied rlimit fallback - #2
Merged
Merged
Conversation
…ied to isolation level
… quota, io limits
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Per-sandbox CPU, memory and process limits, so one runaway or malicious command cannot take down the worker or its neighbours. Before this change only per-process rlimits existed and memory was unbounded.
Design
src/agent_sandbox/cgroups.py): the worker moves itself into aworkerleaf, enablescpu memory pidsfor its subtree, and createssandboxes/<id>withcpu.max,memory.max,memory.swap.max=0andpids.max. Each execution runs in its own leaf (memory.oom.group=1) that it joins before prlimit/setpriv/bwrap, so no descendant can escape. When the execution ends, the leaf is killed (cgroup.kill) and removed. Release, suspend and shutdown remove the sandbox cgroup.unshare --user --map-root-user --mount --cgroupand mounts a fresh read-write cgroup2 rooted at its own cgroup, then uses it via/proc/<helper>/root. This needs only the existingseccomp=unconfined: no capabilities, no privileged mode, no host cgroup mount.workerleaf getsmemory.min=SANDBOX_WORKER_MEMORY_RESERVE_BYTES, and the aggregatesandboxes/memory.maxis set to the container limit minus that reserve.basicandstandarduse cgroups when they can be managed, and otherwise fall back to an rlimit scheme: newRLIMIT_AS, existingRLIMIT_CPU/NPROC/NOFILE/FSIZE, the wall-clock timeout, and killing the sandbox UID's leftover processes.strictrequires cgroups:autodrops tostandard, and an explicitstrictrefuses to start.SANDBOX_CGROUP_MODE=auto|required|off.status=OOM_KILLEDanderror_code=SANDBOX_MEMORY_LIMIT_EXCEEDED. Hitting the pids limit returnserror_code=SANDBOX_PIDS_LIMIT_REACHED./healthzresource_controlreportsscheme, the realsandbox_*_quotaflags, and the effective limits.Defaults
SANDBOX_CGROUP_MODEautoSANDBOX_CPU_QUOTASANDBOX_MEMORY_LIMIT_BYTESSANDBOX_PIDS_LIMITSANDBOX_WORKER_MEMORY_RESERVE_BYTESSANDBOX_ADDRESS_SPACE_LIMIT_BYTES(fallback only)docs/ISOLATION.md has a level → limits table and the outer-runtime requirements.
Verification (Docker, outer container
--memory 1536m --cpus 4, sandbox limits 1 core / 384 MiB / 128 pids)systempaths=unconfined)required)off)off)offresource limits: disabled by SANDBOX_CGROUP_MODE=offThe worker's
/healthzstayed responsive in every case../scripts/integration-test.sh sandboxgives 29 passed, including the newtests/integration/test_resource_limits.py. Unit tests: 755 passed.Found and fixed during verification: at basic/rlimit, fork-bomb survivors kept the sandbox UID's NPROC budget, so the next command failed with
bwrap: Creating new namespace failed. Leftover UID processes are now killed after the sandbox's last execution.Limitations
io.max) limits yet.RLIMIT_AS; JVM, Go and Rust underRLIMIT_ASwere not tested here (the polyglot image wasn't built).resource_control.cgroup.detail).Update: aggregate limits everywhere, plus disk and IO limits
SANDBOX_CGROUP_MODE=off): a supervisor (src/agent_sandbox/supervisor.py) enforces the sandbox's memory and CPU totals across all of its processes. Every sandbox has its own UID, so it tracks that UID:SANDBOX_MEMORY_LIMIT_EXCEEDED./proc/<pid>/io, duty-cycled the same way; this needsCAP_SYS_PTRACE.RLIMIT_ASstays as a hard backstop, andRLIMIT_NPROCon the per-sandbox UID is already an aggregate kernel limit.SANDBOX_DISK_LIMIT_BYTES, default 10 GiB): if usage goes over the quota and keeps growing, the running executions are killed withSANDBOX_DISK_QUOTA_EXCEEDED. File API writes are refused with HTTP 507. Deleting is always allowed.io.maxis set on the workspace block device, or onSANDBOX_IO_DEVICE(default 256 MiB/s read and write)./healthz:resource_control.enforcementsays, for each dimension, whether the kernel enforces it, the supervisor enforces it (and how precisely), or nothing does.Verified in Docker, 17 checks per run:
io.max20.3 MiB/s at a 20 MiB/s limit, CPU 4.0 s in 4.2 s wall at 1 core/sys/fs/cgroup)SYS_PTRACE, IO 19.9 MiB/s at 20 MiB/s-Xmx4gheap bomb returns OOM_KILLEDseccompProfile: Unconfined, runAsUser 0Integration suite: 37 passed, with both schemes parameterized. Unit tests: 762 passed.