How Sandlock Works
Sandlock is not a runtime the workload runs inside. It is a policy the kernel applies to a process, installed after fork() and before exec(), so the workload has never executed a single unconfined instruction.
What Enforces What
Sandlock pushes as much of the policy as possible into the kernel. Only the rules the kernel cannot express, such as "this destination IP" or "this HTTP path", reach the supervisor.
Landlock
KernelLandlock is Linux's unprivileged access-control LSM. Sandlock translates the policy's filesystem, network, and IPC rules into a Landlock ruleset and applies it to the child process. From that point the kernel evaluates every access against the ruleset, and the ruleset can never be relaxed for the lifetime of the process tree.
- Filesystem. Read and write grants are recursive
PATH_BENEATHrules. Because the kernel resolves the path itself at access time, these rules are immune to time-of-check/time-of-use attacks that defeat path-string filtering in userspace. - TCP. Connect and bind port allowlists (Landlock ABI v4 and later).
- IPC. Abstract UNIX socket scoping and signal scoping (ABI v6), which keep a sandbox from reaching sibling processes on the host.
Landlock costs nothing per syscall beyond the kernel's own check. This is why confinement is cheap enough to leave on for production workloads.
seccomp-bpf
KernelA seccomp-bpf filter removes syscall families a confined workload has no legitimate use for, before any of them can reach the kernel's implementation. Sandlock always applies its default blocklist; a policy can add more denials on top of it, and can re-enable specific named groups such as System V IPC when a workload genuinely needs them.
The same filter installs the notification listener used by layer 3. Because the filter is installed alongside NO_NEW_PRIVS, the child cannot regain privileges through a setuid binary, and the filter is inherited by every descendant.
A no-supervisor mode uses only layers 1 and 2: Landlock plus a kernel-only deny filter, with no supervisor process at all. It is the mode used when nesting one sandlock inside another, since the kernel permits only one notification listener per task.
seccomp user notification
SupervisorSome policy cannot be expressed as a static kernel rule. "Connect only to this IP", "stop at 512 MB of resident memory", "stage this write instead of performing it" all require a decision at the moment the syscall happens. Seccomp user notification hands those syscalls to an async supervisor running in the parent process.
The supervisor is the one component that is not the kernel, so Sandlock keeps its responsibilities narrow and its ordering fixed: built-in handlers run first, and any handlers a downstream crate registers run after them, which means a custom handler can extend confinement but never subvert it.
The Confinement Sequence
Order matters. Every restriction is in place before the workload's first instruction runs, and the parent only starts supervising after the child has confirmed the filter is installed.
fork()- 1
setpgid(0,0) - 2
chdir(cwd), if the policy sets one - 3
NO_NEW_PRIVSa setuid binary can now confer nothing - 4
Landlockruleset appliedfilesystem, network, and IPC - 5
seccompfilter installeddeny list plus the notification listener, whose descriptor goes to the parent - 6Wait for the parent's ready signal
- 7Close every descriptor above stderr
- 8
exec(cmd)the workload's first instruction, already confined
- Receives the notification descriptor
- Starts the supervisor on tokio
- Patches the vDSO, if time is frozen
- Starts the policy callback thread, if one is set
- Starts the CPU throttle task, if one is set
Everything on the left happens before the workload exists. Everything on the right supervises it once it does.
Why fds are closed at step 7. A descriptor inherited from the parent is a capability the Landlock ruleset never sees: it was opened before confinement, so the kernel will honour it. Closing everything above stderr removes that escape hatch before exec.
Which Syscalls Take a Round Trip
Only the syscalls in this table are intercepted, and only when the corresponding policy feature is active. Everything else runs at full speed under the kernel filter.
| Syscall | What the supervisor does |
|---|---|
clone / fork / vfork | Enforces the concurrent process count |
mmap / munmap / brk / mremap | Tracks the memory limit |
connect / sendto / sendmsg | Destination IP allowlist, on-behalf execution, HTTP ACL redirect |
bind | On-behalf bind and port remapping |
openat | /proc virtualization and COW interception |
unlinkat / mkdirat / renameat2 | COW write interception |
execve / execveat | Policy callback hold and vDSO re-patching |
getrandom | Deterministic PRNG injection |
clock_nanosleep / timer_settime | Timer adjustment when time is frozen |
getdents64 | PID filtering and COW directory merging |
getsockname | Port remap translation |
Two Paths, Chosen by Your Policy
Sandlock picks the cheapest enforcement path that can express the rules you wrote. You do not configure this; it falls out of the policy.
Direct path
Selected when the policy is pure TCP port rules with no concrete host, IP, or CIDR, and no HTTP ACL. Landlock enforces the port allowlist in the kernel, and there is no per-syscall overhead at all.
--net-allow :443On-behalf path
Selected for any host, IP, or CIDR target, any UDP or ICMP rule, or any HTTP ACL rule, because the destination address has to be inspected and Landlock cannot do that. The supervisor duplicates the child's socket, learns its protocol via SO_PROTOCOL, checks the destination, and performs the syscall itself.
--net-allow api.example.com:443One Core, Many Front Ends
Sandlock is written in Rust. Every interface, including the SDKs in other languages, drives the same core through the same policy object.
The CLI and both SDKs reach the core through the C ABI. sandlock-oci links the core directly, since it is a Rust crate itself and has no reason to cross the boundary.
| Component | Role |
|---|---|
sandlock-core | Rust library: Landlock, seccomp, supervisor, COW, pipelines |
sandlock-cli | The sandlock binary |
sandlock-oci | OCI runtime shim for containerd, CRI-O, and Kubernetes |
sandlock-ffi | C ABI shared library, libsandlock_ffi.so |
| Python SDK | ctypes bindings over the FFI library, plus an MCP server |
| Go SDK | cgo bindings over the FFI library |
What the Host Kernel Must Provide
Sandlock targets Linux 6.12 and later, which is where Landlock ABI v6 lands. Individual protections can be degraded or disabled per policy when you deploy across a mixed fleet.
| Capability | Minimum kernel |
|---|---|
| seccomp user notification | 5.6 |
| Landlock filesystem rules | 5.13 |
| Landlock TCP port rules (ABI v4) | 6.7 |
| Landlock IPC scoping (ABI v6) | 6.12 |
Run sandlock check to report the running kernel's Landlock ABI and which protections are available. Building Sandlock itself needs Rust 1.70 or later; the Python SDK needs Python 3.8 or later.
Read the Documentation
Every policy field, every flag, and every enforcement rule is documented, from a five-minute quickstart to the complete sandbox reference.