Expand description
Sandbox your program with the Linux Landlock LSM.
This crate is a high-level wrapper around the Landlock security module: build a policy, apply it, and let the kernel enforce it. Landlock is unprivileged: no root, no namespace, no seccomp, and it cannot be removed once enforced (it can only be made stricter).
This crate only exists on Linux and Android: on any other target it compiles to an empty crate and using its items fails to compile.
§Quick start
use landlock::{Access, NetworkAccess, new_best_effort};
// The example is not run: restricting a program is irreversible.
let abi = new_best_effort()
.allow_filesystem(Access::Read, ["/etc", "/usr", "/lib"])
.allow_filesystem(Access::ReadWrite, ["/tmp"])
.allow_network(NetworkAccess::ConnectTcp, [443])
.restrict_self()?;
println!("sandboxed (Landlock ABI {abi})");Call Ruleset::restrict_self as early as possible
and before spawning threads (see Threads).
§Everything is denied by default
A fresh policy has no rules, and only rules grant access. Everything the running kernel can restrict is restricted:
- filesystem access (
Access), - network access (
NetworkAccess), - IPC scoping: connections to abstract UNIX sockets and signals sent to
processes outside the sandbox (
Scope).
Ruleset::allow_scope lifts the IPC restrictions for programs that need to
talk to services outside the sandbox (D-Bus, X11, supervisors); filesystem
and network access always stay denied until explicitly allowed.
§Strict or best effort
Each rule is checked against the features the running kernel actually supports (its Landlock ABI version, see Kernel support):
new_strictfails the whole policy if a rule cannot be enforced as requested. What you wrote is what is enforced, or you getError::Unsupported.new_best_effortdrops the rights the kernel cannot enforce and applies the rest. Rules that end up with no access right at all are skipped, which leaves the affected path denied.
Both stop on invalid input (a missing path, an access right that does not
apply to the object, …): Ruleset::restrict_self then returns the first
error and nothing is enforced.
§Filesystem rules
use landlock::{Access, new_best_effort};
let _abi = new_best_effort()
.allow_filesystem(Access::Read, ["/etc", "/usr"])
.allow_filesystem(Access::ReadWrite, ["/tmp"])
.allow_filesystem(Access::Execute, ["/usr/bin"])
.restrict_self()?;Ruleset::allow_filesystemopens each path withO_PATH, which works even for files and directories the process cannot read.Ruleset::allow_file_descriptorgrants access through a file descriptor you already opened, for setups that need to control how the path is resolved.- Rights that do not apply to the object are dropped:
Access::Writeon a regular file grants writing and truncating, notAccess::RemoveFile(which applies to the containing directory). - Paths are resolved by the kernel when the rule is added, so symlinks are followed. Rules are attached to the objects (inodes) rather than to the path, so later renames or bind mounts cannot move a rule somewhere else.
- Rule directories are self-sufficient: a rule on
/aand a rule on/a/bare unrelated, so granting read-write on/tmpand read-write on/tmp/importantis not enough for/tmp/important— grant it explicitly.
§Network rules
Landlock can restrict binding and connecting TCP and UDP sockets, but only
one port per rule: there is no wildcard, so a program that talks to
arbitrary ports has to enumerate them. Network access is denied by default:
a policy that never calls allow_network cannot bind or connect anything.
use landlock::{NetworkAccess, new_best_effort};
let _abi = new_best_effort()
// A DNS client: connect to the resolver, and bind an ephemeral source
// port for UDP (port 0 means the ephemeral port range).
.allow_network(NetworkAccess::ConnectTcp, [53])
.allow_network(NetworkAccess::ConnectSendUdp, [53])
.allow_network(NetworkAccess::BindUdp, [0])
// A server listening on a privileged port.
.allow_network(NetworkAccess::BindTcp, [8080])
.restrict_self()?;UDP datagrams need NetworkAccess::BindUdp on port 0 because sending
auto-binds an ephemeral source port. Multipath TCP and non-TCP/UDP socket
families are not restricted by Landlock.
§Scope: abstract UNIX sockets and signals
Alongside filesystem and network access, Landlock can isolate the sandbox from IPC with processes outside it (Landlock ABI 6, Linux 6.12):
- connections to abstract UNIX sockets created outside the sandbox are
denied with
EPERM(X11’s@/tmp/.X11-unix/X0, D-Bus session buses, accessibility buses, …), - signals sent to processes outside the sandbox are denied with
EPERM(signalling children and threads still works).
Unlike filesystem and network access, Landlock offers no per-object
exception for these: a rule cannot re-open a specific socket or process.
The only knob is Ruleset::allow_scope, which lifts a whole scope:
use landlock::{Scope, new_best_effort};
let abi = new_best_effort()
.allow_scope(Scope::AbstractUnixSocket) // allow talking to D-Bus/X11
.allow_scope(Scope::Signal) // allow signalling outside processes
.restrict_self()?;Pathname UNIX sockets (Wayland, the D-Bus system bus, NSS sockets, …) are a
filesystem matter instead: on Landlock ABI 9 (Linux 7.1) and newer they
require the Access::ResolveUnix right, which can be granted for specific
paths. On older kernels they are not restricted at all.
§Threads
Landlock restrictions apply to the calling thread and to its future
children; sibling threads are not affected. This crate passes the TSYNC
flag (Landlock ABI 8, Linux 7.0) whenever the kernel supports it, which
applies the policy to every thread of the process at once. On older kernels:
- call
Ruleset::restrict_selfbefore spawning any thread, otherwise the already-running threads stay unrestricted; - threads created afterwards inherit the restriction.
Note that TSYNC replaces the policy of the sibling threads, as
documented for landlock_restrict_self(2): in a program whose threads each
applied their own policy, the last one applied ends up in effect
everywhere. Policies applied by the same thread stack instead (they
intersect).
restrict_self also sets PR_SET_NO_NEW_PRIVS on the calling thread, which
is required by Landlock and prevents gaining privileges through set-user-ID
or set-group-ID programs.
§Kernel support
Landlock is available since Linux 5.13, but each version only knows about
the access rights and features introduced up to that point. The ABI version
below is reported by the kernel and returned by Ruleset::restrict_self:
| ABI | Introduced in | Adds |
|---|---|---|
| 1 | Linux 5.13 | basic filesystem rights (Access::Execute … Access::MakeSym) |
| 2 | Linux 5.19 | Access::Refer (link/rename across directories) |
| 3 | Linux 6.2 | Access::Truncate |
| 4 | Linux 6.7 | NetworkAccess::BindTcp, NetworkAccess::ConnectTcp |
| 5 | Linux 6.10 | Access::IoctlDev |
| 6 | Linux 6.12 | Scope::AbstractUnixSocket, Scope::Signal |
| 7 | Linux 6.15 | audit logging flags, errata reporting |
| 8 | Linux 7.0 | process-wide enforcement (restrict_self uses TSYNC) |
| 9 | Linux 7.1 | Access::ResolveUnix (pathname UNIX sockets) |
| 10 | Linux 7.2 | NetworkAccess::BindUdp, NetworkAccess::ConnectSendUdp |
| 11 | Linux 7.3 | atomic no_new_privs flag (this crate falls back to prctl) |
The minimum supported kernel is 6.0 (Landlock ABI 2), which means
Access::Write, Access::ReadWrite and Access::All cannot be fully
enforced there in strict mode: truncation and I/O control on devices need a
newer kernel. Use new_best_effort for portable policies, or request the
individual rights (Access::WriteFile, …) explicitly.
Rights a kernel does not know about cannot be restricted at all: not
handling a right means granting it everywhere. On a kernel older than
Landlock ABI 3, truncation is left unrestricted; before ABI 4 the network is
left alone, and before ABI 6 the IPC scopes have no effect. The ABI version
returned by Ruleset::restrict_self tells what the kernel could actually
enforce. Distributions backport Landlock fixes as well, so the ABI version —
not the kernel version string — is authoritative.
Whether the kernel supports Landlock at all can be checked with
abi_version and is_supported. A kernel may have Landlock compiled in
but disabled at boot time; that is reported the same way.
§Pitfalls
- Files and directories opened before the sandbox is enforced stay usable; pass only the file descriptors you mean to keep. This also means a pre-opened file descriptor can be used to reach a file the policy denies.
exec-ing a program needsAccess::Executeon its binary andAccess::ReadFileon its shared libraries (check withldd).- Some files are opened lazily, after the sandbox is in place: NSS lookups
(
/etc/nsswitch.conf,/etc/hosts,/etc/resolv.conf) are the classic example. Grant them explicitly. - Landlock restricts the filesystem topology (no
mount, nopivot_root) but notchroot, and it does not restrictstat,chmod,chown,fcntl,flockoraccess. - Rights are decided when a file is opened:
stdin/stdout/stderrkeep the rights they had before the sandbox, and file descriptors keep their Landlock properties even when passed to another process. - Denials surface as ordinary
EACCES/EPERMerrors, so check the return value ofRuleset::restrict_selfand usestracewhen a program misbehaves inside the sandbox. - Landlock does not prevent a sandboxed program from exploiting a kernel bug: it restricts the kernel interfaces a process may use, not the kernel itself. Combine it with seccomp for code that parses untrusted input.
Structs§
- Ruleset
- A Landlock policy under construction.
Enums§
- Access
- A filesystem access right.
- Error
- Everything that can prevent a policy from being enforced.
- Feature
- A feature (access right or scope) that the running kernel cannot enforce.
- Network
Access - A network access right.
- Operation
- A Landlock operation that failed with an unexpected error.
- Path
Error - Why a path cannot be used for a rule.
- Scope
- An IPC scope, restricting interactions with processes outside the sandbox.
Functions§
- abi_
version - The Landlock ABI version supported by the running kernel.
- is_
supported - Whether the running kernel supports Landlock at all.
- new_
best_ effort - Creates a policy that drops the rights the running kernel cannot enforce.
- new_
strict - Creates a policy that fails if the running kernel cannot enforce a rule.