2026-07-21
Every sandbox you've used — Docker, Firejail, bubblewrap — needs privileges. seccomp-bpf can block syscalls but knows nothing about filesystem semantics: it can't say "no writes to /home". SELinux and AppArmor need root to load policy. Landlock, merged in Linux 5.13 (2021) and extended through 6.10, is the LSM that lets an unprivileged process shrink its own filesystem and network access, permanently, for itself and its children.
The model is restrict-only. A process creates a ruleset describing what it wants to allow, then calls landlock_restrict_self(). Anything not explicitly allowed inside the ruleset's handled access classes is denied. Rules are additive across nested rulesets (you can only lose access, never gain it), which is why no privilege is needed — like PR_SET_NO_NEW_PRIVS, which landlock requires.
Three syscalls, all added in 5.13:
landlock_create_ruleset(&attr, size, 0) — returns an fd. attr.handled_access_fs is a bitmask like LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_REMOVE_FILE.landlock_add_rule(fd, LANDLOCK_RULE_PATH_BENEATH, &rule, 0) — rule.parent_fd is an fd to a directory (via open(O_PATH)) plus the subset of access rights allowed beneath it.landlock_restrict_self(fd, 0) — enforces the ruleset on the current thread and all descendants. Irreversible.Concrete example. A PDF renderer that should read fonts from /usr/share/fonts, read/write its temp dir, and touch nothing else. You open both directories with O_PATH, add two PATH_BENEATH rules — read-only for fonts, read/write for tempdir — and call landlock_restrict_self. Now open("/etc/shadow", O_RDONLY) returns EACCES. No container, no root, no ptrace tricks. The kernel checks paths against the ruleset on every path_walk.
Rule of thumb for ABI versions. Landlock evolves via a monotonic ABI number: query it with landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION). v1 (5.13) = filesystem read/write/exec. v2 (5.19) = refer/rename across mounts. v3 (6.2) = truncate. v4 (6.7) = TCP bind/connect restrictions. v5 (6.10) = ioctl on device files. Always mask your handled_access_fs with what the running kernel supports, or the ruleset creation fails on older kernels.
The cost is negligible: rules live in a trie keyed by inode, and lookups happen alongside the LSM hook the VFS was already calling. The catch: it operates on inodes at rule-add time, so if you allow /tmp/work and someone renames /tmp/work away, your rule follows the inode, not the path.
