Home
Why Sandlock
How It Works Use Cases Comparison Security Model
Docs
Documentation Home Getting Started CLI Reference Python SDK Sandbox Reference
Products
Overview Sandbox HTTP API Sandbox Scheduler
GitHub Schedule a Demo
Sandlock / Docs /Resources

Resource Limits

How much of the machine a sandbox may consume, and which devices it may reach. All of it without cgroups: the supervisor accounts for allocation as it happens, and device access is a Landlock boundary rather than an environment variable.

The fields

Python TOML CLI Default Caps
max_memorymemory-mnoneResident anonymous memory
max_processesprocesses-P64Concurrent processes
max_open_filesopen_files--max-open-filesnoneOpen descriptors
max_cpucpu-cnonePercentage of one core
max_diskdisk--max-disknoneCOW upper-layer size
cpu_corescpu_cores--cpu-coresnoneWhich cores may run it
num_cpusnum_cpus--num-cpusnoneHow many cores it can see
gpu_devicesgpu_devices--gpunone (denied)Which GPUs it may open

A wall-clock timeout is separate, and CLI-only: -t kills the sandbox after that many seconds. From the SDKs, pass timeout= to the run call.

A typical envelope
$ sandlock run -m 512M -P 20 -t 30 -- ./compute.sh

Memory

There are no cgroups involved. The supervisor intercepts mmap, munmap, brk, and mremap, and tracks the sandbox's allocation against the cap as the calls happen.

Two consequences worth knowing:

Only anonymous mappings count. The accounting is for memory the sandbox actually claims, so a file-backed mapping of a large read-only file is not charged against the limit. This is usually what you want, and it is why a workload that maps a multi-gigabyte model file can still run under a modest cap.

The decisions are made on register arguments. Mapping length and brk addresses arrive in the notification itself rather than through a pointer into the guest's memory, so this accounting is not exposed to the re-read hazard that governs path handling. The one exception is clone3, whose flags live in a struct the supervisor reads from guest memory; that read feeds process accounting only, never a security decision, so a misread can throttle incorrectly but cannot bypass a kernel-enforced deny.

Accepts a string such as "512M" or "1G", or an integer byte count.

Processes

max_processes is a cap on concurrent processes, not on how many the sandbox may create over its lifetime. A build that spawns ten thousand short-lived compilers sequentially runs fine under a limit of 64; ten thousand at once does not.

Threads do not count. A fork-like call carrying CLONE_THREAD is creating a thread, not a process, and is not charged. A workload with a large thread pool is unaffected by this limit.

The default is 64. Setting it also enables the fork interception used by checkpoint freeze, so a policy that needs checkpointing gets it as a side effect of having a process limit at all.

Open files

This one has more edges than the others, and it is worth being precise about what it is.

max_open_files is enforced with RLIMIT_NOFILE, set in the child just before it execs. Both the soft and the hard limit are lowered, and descendants inherit the cap.

Treat this as a budget, not as confinement. Lowering the hard limit is one-way only for an unprivileged Sandlock. A sandbox launched by root, or with CAP_SYS_RESOURCE, can raise it back, because Sandlock does not drop capabilities.

It is an upper bound, never a grant. The value is clamped to both limits Sandlock itself inherited, so requesting more than the inherited soft limit gives the guest the inherited limit rather than the number you asked for. If a guest genuinely needs a bigger budget, raise the limit on Sandlock itself with prlimit or systemd's LimitNOFILE=.

It has to cover process startup. The limit must accommodate stdio, the dynamic loader's per-library descriptors, and under chroot the injected exec descriptor. Set it too low and the exec fails with exit code 127.

When it failsPlain execUnder chroot
At startupEMFILE, exit 127EIO, exit 127
After startup, kernel-serviced openEMFILE
After startup, supervisor-mediated openEACCES (chroot, COW, procfs virtualization)

The measured floor for a trivial command is about 4, for both plain exec and chroot. Programs linking more libraries need more.

CPU

Two independent knobs: how much CPU time the sandbox gets, and which cores it runs on.

Throttling

max_cpu is a percentage of a single core, from 1 to 100. It is applied to the entire process group by cycling SIGSTOP and SIGCONT from a supervisor task.

That mechanism is worth understanding before you rely on it. It is a duty cycle, not a scheduler reservation: the sandbox is repeatedly stopped and resumed to approximate the requested share. It is effective for holding a runaway workload down, and it is not the latency guarantee a cgroup CPU quota gives you. For hard scheduling guarantees, use cgroups on the Sandlock process itself.

Pinning

cpu_cores pins the sandbox to a set of cores through sched_setaffinity in the child. This is a real placement decision made by the kernel, not an approximation.

Throttle to half a core, pinned to cores 0 and 1
$ sandlock run -c 50 --cpu-cores 0,1 \
    -r /usr -r /lib -- ./compute.sh

What the sandbox can see

num_cpus changes the visible CPU count in /proc/cpuinfo, renumbered 0..N-1. This matters more than it sounds: runtimes size their thread pools from what they see there, so a workload on a 128-core host will happily spawn 128 workers inside a sandbox pinned to two cores unless you tell it otherwise.

When max_memory is also set, /proc/meminfo is virtualized to match, so a runtime sizing its heap from available memory sees the sandbox's budget rather than the host's.

Pinning and visibility are separate. cpu_cores decides where the sandbox runs; num_cpus decides what it believes about the machine. Set both when a workload sizes itself from the CPU count.

Disk

max_disk caps the copy-on-write upper layer, so it only applies when workdir is set. Exceeding it surfaces to the workload as ENOSPC, the same error a full disk produces, which means programs handle it through the path they already have for that case.

See storage and quota for where the upper layer lives and how to move it off the source volume.

GPUs

GPU selection is an access-control decision, not a hint. gpu_devices adds Landlock rules for the chosen /dev/nvidia* and /dev/dri/* nodes, and sets CUDA_VISIBLE_DEVICES and ROCR_VISIBLE_DEVICES to match.

The environment variables are a convenience for the frameworks. The boundary is the Landlock rule: a sandbox given device 0 cannot open any other device node, whatever the workload does with its environment. This is the difference between hiding a GPU and denying it.

ValueEffect
None (the default)GPU access denied entirely
[]Every GPU exposed
[0, 2]Only those devices openable

From the CLI, --gpu all exposes every device and --gpu 0,2 selects indices.

One GPU, hard boundary
# The NVIDIA userspace also needs the driver libraries readable, and
# it writes thread names under /proc/self/task.
$ sandlock run --gpu 0 \
    -r /usr -r /lib -r /lib64 -r /etc -r /sys -r /proc \
    -w /proc/self/task \
    -- python3 train.py

The extra paths in that example are not incidental. The NVIDIA userspace stack loads driver libraries from the system directories and writes thread names under /proc/self/task, so a policy that grants only the device node will fail in ways that look like a driver problem rather than a policy one.

Tightening limits at runtime

Memory and process limits can be lowered while the sandbox runs, from a policy callback. A common shape is to give a program the headroom it needs to initialize, then reduce it once startup is done.

python
def on_event(event, ctx):
    if event.syscall == "execve":
        ctx.restrict_max_memory(256 * 1024 * 1024)
        ctx.restrict_max_processes(8)
    return 0

sandbox = Sandbox(
    fs_readable=["/usr", "/lib"],
    max_memory="2G",
    policy_fn=on_event,
)

These only narrow. See Dynamic Policy for the full context API.

What these limits are not

Stating this plainly, because picking the wrong primitive is worse than picking none:

  • Not scheduler guarantees. CPU throttling is a stop/resume duty cycle. It bounds consumption; it does not reserve capacity or bound latency.
  • Not protection against the workload wedging itself. Limits protect the host. A sandbox can still spin, thrash, or deadlock inside its own budget.
  • Not a defence against a privileged launcher. The open-files cap in particular can be raised back by a sandbox launched with the capability to do so.

If you need hard, scheduler-level guarantees, put the Sandlock process itself in a cgroup. The two compose: the cgroup bounds the whole thing from outside, and Sandlock's own limits partition what happens within it.