nono can place hard ceilings on how much a sandboxed command — and every process it spawns — may consume: a memory cap and a process-count cap. Both are enforced by the Linux kernel, and both cover the whole process tree, so a runaway agent cannot exhaust the host.
Limiting memory
Pass--memory to nono run (or nono shell):
So
512M is 536,870,912 bytes and 1Gi is 1,073,741,824 bytes. The ceiling covers the whole process tree, not just the first process.
Limiting the process count
Pass--max-processes to cap how many processes and threads the sandbox may have alive at once:
1). It counts tasks — every process and every thread in the tree — so pick a value with headroom for the threads your workload legitimately uses. This is what bounds fork bombs and runaway spawning.
The two flags are independent and can be combined:
What happens at the limit
Memory. When the tree crosses the memory cap, the kernel terminates the entire sandbox at once andnono explains why instead of dying silently:
137 (128 + SIGKILL). A failure unrelated to memory shows the normal diagnostics, not this one.
Process count. Hitting the process cap is different: the kernel kills nothing. It simply refuses the next fork/clone with EAGAIN, which the program usually surfaces as “resource temporarily unavailable” or “cannot fork”. The exit code is whatever the program itself returns. The kernel’s denied-fork counter is cumulative, so it proves only that the cap was touched, not that it caused the outcome. nono therefore notes it only when the run exits non-zero — a clean exit means the program recovered from the EAGAIN, and a run killed by an unrelated signal is never attributed to the cap (a process breach never kills):
How it works
On Linux the limits are enforced with cgroup v2, the same kernel mechanism containers use. For each runnono:
- creates a leaf control group under your delegated user session (
.../user@<uid>.service/nono.<pid>), - sets the requested knobs:
memory.maxto the memory cap, plusmemory.swap.max=0(so the limit cannot be dodged via swap) andmemory.oom.group=1(so the whole tree is killed together),pids.maxto the process cap,
- has the sandboxed child place itself into the group before it can fork or exec, so every descendant is contained by construction.
SIGKILL) is swept on the next run.
Requirements and scope
- Linux only. Enforcement needs cgroup v2 and a systemd
Delegate=yesuser session (a normal desktop or server login). On other platforms a limit request is refused rather than silently ignored. - Delegated controllers. The relevant controller must be delegated to your user session’s cgroup subtree:
memoryfor--memory,pidsfor--max-processes. If it is not,nonorefuses the run rather than enforce a limit that would silently not apply. Check withcat /sys/fs/cgroup/<...>/cgroup.subtree_control. - Supervised runs only.
nono runandnono shellenforce limits.nono wrapexecs directly and cannot create the cgroup, so it does not accept the resource flags. - Fail closed. If the cgroup cannot be created or armed, the run is refused —
nononever runs a requested limit unenforced.
Manifests
A capability manifest can carry either or both limits. They require the supervised execution strategy:resources.memory_bytes is a plain byte count and resources.max_processes a plain task count — human-friendly sizes like 512M are a CLI convenience, resolved before they reach the manifest. Both have a schema minimum of 1.
Do not grant write over the cgroup tree
The limits live in control files under/sys/fs/cgroup. If the sandbox is also granted write access overlapping that path (for example --allow /sys), the process could rewrite its own limit (memory.max / pids.max) and escape the cap. nono refuses such a run while any resource limit is active:
/sys read-only (--read /sys) if a workload needs it — read access cannot defeat a cap.
Inspecting the limit
The active limits appear in the capability summary at launch, and innono why --self from inside the sandbox: