Skip to main content

Crate landlock

Crate landlock 

Source
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_strict fails the whole policy if a rule cannot be enforced as requested. What you wrote is what is enforced, or you get Error::Unsupported.
  • new_best_effort drops 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_filesystem opens each path with O_PATH, which works even for files and directories the process cannot read.
  • Ruleset::allow_file_descriptor grants 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::Write on a regular file grants writing and truncating, not Access::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 /a and a rule on /a/b are unrelated, so granting read-write on /tmp and read-write on /tmp/important is 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_self before 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:

ABIIntroduced inAdds
1Linux 5.13basic filesystem rights (Access::Execute … Access::MakeSym)
2Linux 5.19Access::Refer (link/rename across directories)
3Linux 6.2Access::Truncate
4Linux 6.7NetworkAccess::BindTcp, NetworkAccess::ConnectTcp
5Linux 6.10Access::IoctlDev
6Linux 6.12Scope::AbstractUnixSocket, Scope::Signal
7Linux 6.15audit logging flags, errata reporting
8Linux 7.0process-wide enforcement (restrict_self uses TSYNC)
9Linux 7.1Access::ResolveUnix (pathname UNIX sockets)
10Linux 7.2NetworkAccess::BindUdp, NetworkAccess::ConnectSendUdp
11Linux 7.3atomic 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 needs Access::Execute on its binary and Access::ReadFile on its shared libraries (check with ldd).
  • 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, no pivot_root) but not chroot, and it does not restrict stat, chmod, chown, fcntl, flock or access.
  • Rights are decided when a file is opened: stdin/stdout/stderr keep 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/EPERM errors, so check the return value of Ruleset::restrict_self and use strace when 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.
NetworkAccess
A network access right.
Operation
A Landlock operation that failed with an unexpected error.
PathError
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.