Skip to main content
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):
Sizes take an optional, case-insensitive unit suffix: 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:
The value is a plain count (minimum 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 and nono explains why instead of dying silently:
The run exits 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 run nono:
  • creates a leaf control group under your delegated user session (.../user@<uid>.service/nono.<pid>),
  • sets the requested knobs:
    • memory.max to the memory cap, plus memory.swap.max=0 (so the limit cannot be dodged via swap) and memory.oom.group=1 (so the whole tree is killed together),
    • pids.max to 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.
The group is removed when the run ends. A leftover group from a supervisor that was hard-killed (SIGKILL) is swept on the next run.

Requirements and scope

  • Linux only. Enforcement needs cgroup v2 and a systemd Delegate=yes user 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: memory for --memory, pids for --max-processes. If it is not, nono refuses the run rather than enforce a limit that would silently not apply. Check with cat /sys/fs/cgroup/<...>/cgroup.subtree_control.
  • Supervised runs only. nono run and nono shell enforce limits. nono wrap execs 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 — nono never 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:
Grant /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 in nono why --self from inside the sandbox: