Skip to main content

landlock/
landlock.rs

1#![cfg(any(target_os = "linux", target_os = "android"))]
2
3//! Sandbox your program with the Linux [Landlock] LSM.
4//!
5//! This crate is a high-level wrapper around the Landlock
6//! security module: build a policy, apply it, and let the kernel enforce it.
7//! Landlock is unprivileged: no root, no namespace, no seccomp, and it cannot
8//! be removed once enforced (it can only be made stricter).
9//!
10//! This crate only exists on Linux and Android: on any other target it
11//! compiles to an empty crate and using its items fails to compile.
12//!
13//! # Quick start
14//!
15//! ```
16//! use landlock::{Access, NetworkAccess, new_best_effort};
17//!
18//! # fn main() -> Result<(), landlock::Error> {
19//! // The example is not run: restricting a program is irreversible.
20//! # if false {
21//! let abi = new_best_effort()
22//!     .allow_filesystem(Access::Read, ["/etc", "/usr", "/lib"])
23//!     .allow_filesystem(Access::ReadWrite, ["/tmp"])
24//!     .allow_network(NetworkAccess::ConnectTcp, [443])
25//!     .restrict_self()?;
26//!
27//! println!("sandboxed (Landlock ABI {abi})");
28//! # }
29//! # Ok(())
30//! # }
31//! ```
32//!
33//! Call [`Ruleset::restrict_self`] as early as possible
34//! and before spawning threads (see [Threads](#threads)).
35//!
36//! # Everything is denied by default
37//!
38//! A fresh policy has no rules, and **only rules grant access**. Everything the
39//! running kernel can restrict is restricted:
40//!
41//! * filesystem access (`Access`),
42//! * network access (`NetworkAccess`),
43//! * IPC scoping: connections to abstract UNIX sockets and signals sent to
44//!   processes outside the sandbox ([`Scope`]).
45//!
46//! [`Ruleset::allow_scope`] lifts the IPC restrictions for programs that need to
47//! talk to services outside the sandbox (D-Bus, X11, supervisors); filesystem
48//! and network access always stay denied until explicitly allowed.
49//!
50//! # Strict or best effort
51//!
52//! Each rule is checked against the features the running kernel actually
53//! supports (its Landlock ABI version, see [Kernel support](#kernel-support)):
54//!
55//! * [`new_strict`] fails the whole policy if a rule cannot be enforced as
56//!   requested. What you wrote is what is enforced, or you get
57//!   [`Error::Unsupported`].
58//! * [`new_best_effort`] drops the rights the kernel cannot enforce and applies
59//!   the rest. Rules that end up with no access right at all are skipped, which
60//!   leaves the affected path denied.
61//!
62//! Both stop on invalid input (a missing path, an access right that does not
63//! apply to the object, …): [`Ruleset::restrict_self`] then returns the first
64//! error and **nothing is enforced**.
65//!
66//! # Filesystem rules
67//!
68//! ```no_run
69//! # fn main() -> Result<(), landlock::Error> {
70//! use landlock::{Access, new_best_effort};
71//!
72//! let _abi = new_best_effort()
73//!     .allow_filesystem(Access::Read, ["/etc", "/usr"])
74//!     .allow_filesystem(Access::ReadWrite, ["/tmp"])
75//!     .allow_filesystem(Access::Execute, ["/usr/bin"])
76//!     .restrict_self()?;
77//! # Ok(())
78//! # }
79//! ```
80//!
81//! * [`Ruleset::allow_filesystem`] opens each path with `O_PATH`, which works
82//!   even for files and directories the process cannot read.
83//! * [`Ruleset::allow_file_descriptor`] grants access through a file descriptor
84//!   you already opened, for setups that need to control how the path is
85//!   resolved.
86//! * Rights that do not apply to the object are dropped: `Access::Write` on a
87//!   regular file grants writing and truncating, not `Access::RemoveFile` (which
88//!   applies to the containing directory).
89//! * Paths are resolved by the kernel when the rule is added, so **symlinks are
90//!   followed**. Rules are attached to the objects (inodes) rather than to the
91//!   path, so later renames or bind mounts cannot move a rule somewhere else.
92//! * Rule directories are *self-sufficient*: a rule on `/a` and a rule on
93//!   `/a/b` are unrelated, so granting read-write on `/tmp` and read-write on
94//!   `/tmp/important` is not enough for `/tmp/important` — grant it explicitly.
95//!
96//! # Network rules
97//!
98//! Landlock can restrict binding and connecting TCP and UDP sockets, but only
99//! **one port per rule**: there is no wildcard, so a program that talks to
100//! arbitrary ports has to enumerate them. Network access is denied by default:
101//! a policy that never calls `allow_network` cannot bind or connect anything.
102//!
103//! ```no_run
104//! # fn main() -> Result<(), landlock::Error> {
105//! use landlock::{NetworkAccess, new_best_effort};
106//!
107//! let _abi = new_best_effort()
108//!     // A DNS client: connect to the resolver, and bind an ephemeral source
109//!     // port for UDP (port 0 means the ephemeral port range).
110//!     .allow_network(NetworkAccess::ConnectTcp, [53])
111//!     .allow_network(NetworkAccess::ConnectSendUdp, [53])
112//!     .allow_network(NetworkAccess::BindUdp, [0])
113//!     // A server listening on a privileged port.
114//!     .allow_network(NetworkAccess::BindTcp, [8080])
115//!     .restrict_self()?;
116//! # Ok(())
117//! # }
118//! ```
119//!
120//! UDP datagrams need `NetworkAccess::BindUdp` on port 0 because sending
121//! auto-binds an ephemeral source port. Multipath TCP and non-TCP/UDP socket
122//! families are not restricted by Landlock.
123//!
124//! # Scope: abstract UNIX sockets and signals
125//!
126//! Alongside filesystem and network access, Landlock can isolate the sandbox
127//! from IPC with processes outside it (Landlock ABI 6, Linux 6.12):
128//!
129//! * connections to **abstract** UNIX sockets created outside the sandbox are
130//!   denied with `EPERM` (X11's `@/tmp/.X11-unix/X0`, D-Bus session buses,
131//!   accessibility buses, …),
132//! * signals sent to processes outside the sandbox are denied with `EPERM`
133//!   (signalling children and threads still works).
134//!
135//! Unlike filesystem and network access, Landlock offers **no per-object
136//! exception** for these: a rule cannot re-open a specific socket or process.
137//! The only knob is [`Ruleset::allow_scope`], which lifts a whole scope:
138//!
139//! ```no_run
140//! # fn main() -> Result<(), landlock::Error> {
141//! use landlock::{Scope, new_best_effort};
142//!
143//! let abi = new_best_effort()
144//!     .allow_scope(Scope::AbstractUnixSocket) // allow talking to D-Bus/X11
145//!     .allow_scope(Scope::Signal)             // allow signalling outside processes
146//!     .restrict_self()?;
147//! # Ok(())
148//! # }
149//! ```
150//!
151//! Pathname UNIX sockets (Wayland, the D-Bus system bus, NSS sockets, …) are a
152//! filesystem matter instead: on Landlock ABI 9 (Linux 7.1) and newer they
153//! require the `Access::ResolveUnix` right, which *can* be granted for specific
154//! paths. On older kernels they are not restricted at all.
155//!
156//! # Threads
157//!
158//! Landlock restrictions apply to the **calling thread** and to its future
159//! children; sibling threads are not affected. This crate passes the `TSYNC`
160//! flag (Landlock ABI 8, Linux 7.0) whenever the kernel supports it, which
161//! applies the policy to every thread of the process at once. On older kernels:
162//!
163//! * call [`Ruleset::restrict_self`] **before spawning any thread**, otherwise
164//!   the already-running threads stay unrestricted;
165//! * threads created afterwards inherit the restriction.
166//!
167//! Note that `TSYNC` *replaces* the policy of the sibling threads, as
168//! documented for `landlock_restrict_self(2)`: in a program whose threads each
169//! applied their own policy, the last one applied ends up in effect
170//! everywhere. Policies applied by the *same* thread stack instead (they
171//! intersect).
172//!
173//! `restrict_self` also sets `PR_SET_NO_NEW_PRIVS` on the calling thread, which
174//! is required by Landlock and prevents gaining privileges through set-user-ID
175//! or set-group-ID programs.
176//!
177//! # Kernel support
178//!
179//! Landlock is available since Linux 5.13, but each version only knows about
180//! the access rights and features introduced up to that point. The ABI version
181//! below is reported by the kernel and returned by [`Ruleset::restrict_self`]:
182//!
183//! | ABI | Introduced in | Adds |
184//! |-----|---------------|------|
185//! | 1 | Linux 5.13 | basic filesystem rights (`Access::Execute` … `Access::MakeSym`) |
186//! | 2 | Linux 5.19 | `Access::Refer` (link/rename across directories) |
187//! | 3 | Linux 6.2 | `Access::Truncate` |
188//! | 4 | Linux 6.7 | `NetworkAccess::BindTcp`, `NetworkAccess::ConnectTcp` |
189//! | 5 | Linux 6.10 | `Access::IoctlDev` |
190//! | 6 | Linux 6.12 | `Scope::AbstractUnixSocket`, `Scope::Signal` |
191//! | 7 | Linux 6.15 | audit logging flags, errata reporting |
192//! | 8 | Linux 7.0 | process-wide enforcement (`restrict_self` uses `TSYNC`) |
193//! | 9 | Linux 7.1 | `Access::ResolveUnix` (pathname UNIX sockets) |
194//! | 10 | Linux 7.2 | `NetworkAccess::BindUdp`, `NetworkAccess::ConnectSendUdp` |
195//! | 11 | Linux 7.3 | atomic `no_new_privs` flag (this crate falls back to `prctl`) |
196//!
197//! The minimum supported kernel is **6.0** (Landlock ABI 2), which means
198//! `Access::Write`, `Access::ReadWrite` and `Access::All` cannot be fully
199//! enforced there in strict mode: truncation and I/O control on devices need a
200//! newer kernel. Use [`new_best_effort`] for portable policies, or request the
201//! individual rights (`Access::WriteFile`, …) explicitly.
202//!
203//! Rights a kernel does not know about cannot be restricted **at all**: not
204//! handling a right means granting it everywhere. On a kernel older than
205//! Landlock ABI 3, truncation is left unrestricted; before ABI 4 the network is
206//! left alone, and before ABI 6 the IPC scopes have no effect. The ABI version
207//! returned by [`Ruleset::restrict_self`] tells what the kernel could actually
208//! enforce. Distributions backport Landlock fixes as well, so the ABI version —
209//! not the kernel version string — is authoritative.
210//!
211//! Whether the kernel supports Landlock at all can be checked with
212//! [`abi_version`] and [`is_supported`]. A kernel may have Landlock compiled in
213//! but disabled at boot time; that is reported the same way.
214//!
215//! # Pitfalls
216//!
217//! * Files and directories **opened before** the sandbox is enforced stay
218//!   usable; pass only the file descriptors you mean to keep. This also means a
219//!   pre-opened file descriptor can be used to reach a file the policy denies.
220//! * `exec`-ing a program needs `Access::Execute` on its binary **and**
221//!   `Access::ReadFile` on its shared libraries (check with `ldd`).
222//! * Some files are opened lazily, *after* the sandbox is in place: NSS lookups
223//!   (`/etc/nsswitch.conf`, `/etc/hosts`, `/etc/resolv.conf`) are the classic
224//!   example. Grant them explicitly.
225//! * Landlock restricts the filesystem topology (no `mount`, no `pivot_root`)
226//!   but not `chroot`, and it does not restrict `stat`, `chmod`, `chown`,
227//!   `fcntl`, `flock` or `access`.
228//! * Rights are decided when a file is opened: `stdin`/`stdout`/`stderr` keep
229//!   the rights they had before the sandbox, and file descriptors keep their
230//!   Landlock properties even when passed to another process.
231//! * Denials surface as ordinary `EACCES`/`EPERM` errors, so check the return
232//!   value of [`Ruleset::restrict_self`] and use `strace` when a program
233//!   misbehaves inside the sandbox.
234//! * Landlock does not prevent a sandboxed program from exploiting a kernel
235//!   bug: it restricts the kernel interfaces a process may use, not the kernel
236//!   itself. Combine it with seccomp for code that parses untrusted input.
237//!
238//! [Landlock]: https://docs.kernel.org/userspace-api/landlock.html
239
240mod sys;
241
242use std::{
243    error::Error as StdError,
244    fmt, io,
245    os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd},
246    path::{Path, PathBuf},
247};
248
249/// The largest path the kernel accepts, including the terminating NUL byte.
250const PATH_MAX: usize = 4096;
251
252/// `EBADFD` ("file descriptor in bad state", errno 77) is Linux-specific: the
253/// kernel returns it from `landlock_add_rule` when the file descriptor does not
254/// identify a file hierarchy, or refers to an internal filesystem. `libc` does
255/// not define it for every platform this crate compiles on.
256const EBADFD: i32 = 77;
257
258// Access rights that may be granted on a non-directory (`ACCESS_FILE` in the
259// kernel): the other rights only make sense for directories.
260const FS_FILE_BITS: u64 = sys::FS_EXECUTE
261    | sys::FS_WRITE_FILE
262    | sys::FS_READ_FILE
263    | sys::FS_TRUNCATE
264    | sys::FS_IOCTL_DEV
265    | sys::FS_RESOLVE_UNIX;
266
267const FS_READ_BITS: u64 = sys::FS_READ_FILE | sys::FS_READ_DIR;
268
269const FS_WRITE_BITS: u64 = sys::FS_WRITE_FILE
270    | sys::FS_TRUNCATE
271    | sys::FS_REMOVE_DIR
272    | sys::FS_REMOVE_FILE
273    | sys::FS_MAKE_CHAR
274    | sys::FS_MAKE_DIR
275    | sys::FS_MAKE_REG
276    | sys::FS_MAKE_SOCK
277    | sys::FS_MAKE_FIFO
278    | sys::FS_MAKE_BLOCK
279    | sys::FS_MAKE_SYM
280    | sys::FS_REFER;
281
282const FS_ALL_BITS: u64 = FS_READ_BITS | FS_WRITE_BITS | sys::FS_EXECUTE | sys::FS_IOCTL_DEV | sys::FS_RESOLVE_UNIX;
283
284const NET_TCP_BITS: u32 = sys::NET_BIND_TCP | sys::NET_CONNECT_TCP;
285
286const NET_UDP_BITS: u32 = sys::NET_BIND_UDP | sys::NET_CONNECT_SEND_UDP;
287
288const NET_ALL_BITS: u32 = NET_TCP_BITS | NET_UDP_BITS;
289
290const SCOPE_ALL_BITS: u32 = sys::SCOPE_ABSTRACT_UNIX_SOCKET | sys::SCOPE_SIGNAL;
291
292/// For every filesystem access right, the Landlock ABI that introduced it.
293const FS_ABI_TABLE: [(u64, u32); 17] = [
294    (sys::FS_EXECUTE, 1),
295    (sys::FS_WRITE_FILE, 1),
296    (sys::FS_READ_FILE, 1),
297    (sys::FS_READ_DIR, 1),
298    (sys::FS_REMOVE_DIR, 1),
299    (sys::FS_REMOVE_FILE, 1),
300    (sys::FS_MAKE_CHAR, 1),
301    (sys::FS_MAKE_DIR, 1),
302    (sys::FS_MAKE_REG, 1),
303    (sys::FS_MAKE_SOCK, 1),
304    (sys::FS_MAKE_FIFO, 1),
305    (sys::FS_MAKE_BLOCK, 1),
306    (sys::FS_MAKE_SYM, 1),
307    (sys::FS_REFER, 2),
308    (sys::FS_TRUNCATE, 3),
309    (sys::FS_IOCTL_DEV, 5),
310    (sys::FS_RESOLVE_UNIX, 9),
311];
312
313/// For every network access right, the Landlock ABI that introduced it.
314const NET_ABI_TABLE: [(u32, u32); 4] = [
315    (sys::NET_BIND_TCP, 4),
316    (sys::NET_CONNECT_TCP, 4),
317    (sys::NET_BIND_UDP, 10),
318    (sys::NET_CONNECT_SEND_UDP, 10),
319];
320
321/// For every scope, the Landlock ABI that introduced it.
322const SCOPE_ABI_TABLE: [(u32, u32); 2] = [(sys::SCOPE_ABSTRACT_UNIX_SOCKET, 6), (sys::SCOPE_SIGNAL, 6)];
323
324/// Filesystem rights supported by Landlock ABI `abi`.
325const fn fs_supported(abi: u32) -> u64 {
326    let mut mask = 0u64;
327    let mut i = 0;
328    while i < FS_ABI_TABLE.len() {
329        if abi >= FS_ABI_TABLE[i].1 {
330            mask |= FS_ABI_TABLE[i].0;
331        }
332        i += 1;
333    }
334    mask
335}
336
337/// Network rights supported by Landlock ABI `abi`.
338const fn net_supported(abi: u32) -> u32 {
339    let mut mask = 0u32;
340    let mut i = 0;
341    while i < NET_ABI_TABLE.len() {
342        if abi >= NET_ABI_TABLE[i].1 {
343            mask |= NET_ABI_TABLE[i].0;
344        }
345        i += 1;
346    }
347    mask
348}
349
350/// Scopes supported by Landlock ABI `abi`.
351const fn scope_supported(abi: u32) -> u32 {
352    let mut mask = 0u32;
353    let mut i = 0;
354    while i < SCOPE_ABI_TABLE.len() {
355        if abi >= SCOPE_ABI_TABLE[i].1 {
356            mask |= SCOPE_ABI_TABLE[i].0;
357        }
358        i += 1;
359    }
360    mask
361}
362
363/// The oldest Landlock ABI required to enforce every bit of `bits`, or 0 if
364/// `bits` is empty.
365const fn min_abi_fs(bits: u64) -> u32 {
366    let mut abi = 0u32;
367    let mut i = 0;
368    while i < FS_ABI_TABLE.len() {
369        let (bit, introduced) = FS_ABI_TABLE[i];
370        if bits & bit != 0 && introduced > abi {
371            abi = introduced;
372        }
373        i += 1;
374    }
375    abi
376}
377
378/// Same as [`min_abi_fs`], for network rights.
379const fn min_abi_net(bits: u32) -> u32 {
380    let mut abi = 0u32;
381    let mut i = 0;
382    while i < NET_ABI_TABLE.len() {
383        let (bit, introduced) = NET_ABI_TABLE[i];
384        if bits & bit != 0 && introduced > abi {
385            abi = introduced;
386        }
387        i += 1;
388    }
389    abi
390}
391
392/// The kernel version that introduced `abi`, when it is known.
393const fn abi_kernel_version(abi: u32) -> Option<&'static str> {
394    match abi {
395        1 => Some("5.13"),
396        2 => Some("5.19"),
397        3 => Some("6.2"),
398        4 => Some("6.7"),
399        5 => Some("6.10"),
400        6 => Some("6.12"),
401        7 => Some("6.15"),
402        8 => Some("7.0"),
403        9 => Some("7.1"),
404        10 => Some("7.2"),
405        11 => Some("7.3"),
406        _ => None,
407    }
408}
409
410/// A filesystem access right.
411///
412/// Values are either a single right or a group of related rights. Because this
413/// is an enumeration (and not a raw bitmask), a policy cannot contain bits the
414/// kernel does not define.
415///
416/// The groups require the Landlock ABI that introduced their newest member:
417/// [`Access::Read`] needs ABI 1, [`Access::Write`] and [`Access::ReadWrite`]
418/// need ABI 3 (`Access::Truncate`) and [`Access::All`] needs ABI 9
419/// (`Access::ResolveUnix`). [`new_strict`] refuses them on older kernels, while
420/// [`new_best_effort`] drops what the kernel does not support.
421#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
422#[non_exhaustive]
423pub enum Access {
424    /// Execute a file.
425    Execute,
426    /// Open a file for writing.
427    WriteFile,
428    /// Open a file for reading.
429    ReadFile,
430    /// Open a directory or list its content.
431    ReadDir,
432    /// Remove an empty directory, or rename one.
433    RemoveDir,
434    /// Unlink a file, or rename one.
435    RemoveFile,
436    /// Create (or rename or link) a character device.
437    MakeChar,
438    /// Create (or rename) a directory.
439    MakeDir,
440    /// Create (or rename or link) a regular file.
441    MakeReg,
442    /// Create (or rename or link) a UNIX domain socket.
443    MakeSock,
444    /// Create (or rename or link) a named pipe.
445    MakeFifo,
446    /// Create (or rename or link) a block device.
447    MakeBlock,
448    /// Create (or rename or link) a symbolic link.
449    MakeSym,
450    /// Link or rename a file to or from another directory.
451    Refer,
452    /// Truncate a file (`truncate`, `ftruncate`, `creat`, `open` with
453    /// `O_TRUNC`).
454    Truncate,
455    /// Invoke `ioctl` commands on an opened character or block device.
456    IoctlDev,
457    /// Look up pathname UNIX domain sockets.
458    ResolveUnix,
459    /// Read access to files and directories ([`Access::ReadFile`] and
460    /// [`Access::ReadDir`]).
461    Read,
462    /// Write access: everything that modifies the content of a directory
463    /// ([`Access::WriteFile`], [`Access::Truncate`], [`Access::RemoveDir`],
464    /// [`Access::RemoveFile`], the `Make*` rights and [`Access::Refer`]).
465    Write,
466    /// [`Access::Read`] and [`Access::Write`].
467    ReadWrite,
468    /// Every access right.
469    All,
470}
471
472impl Access {
473    const fn bits(self) -> u64 {
474        match self {
475            Access::Execute => sys::FS_EXECUTE,
476            Access::WriteFile => sys::FS_WRITE_FILE,
477            Access::ReadFile => sys::FS_READ_FILE,
478            Access::ReadDir => sys::FS_READ_DIR,
479            Access::RemoveDir => sys::FS_REMOVE_DIR,
480            Access::RemoveFile => sys::FS_REMOVE_FILE,
481            Access::MakeChar => sys::FS_MAKE_CHAR,
482            Access::MakeDir => sys::FS_MAKE_DIR,
483            Access::MakeReg => sys::FS_MAKE_REG,
484            Access::MakeSock => sys::FS_MAKE_SOCK,
485            Access::MakeFifo => sys::FS_MAKE_FIFO,
486            Access::MakeBlock => sys::FS_MAKE_BLOCK,
487            Access::MakeSym => sys::FS_MAKE_SYM,
488            Access::Refer => sys::FS_REFER,
489            Access::Truncate => sys::FS_TRUNCATE,
490            Access::IoctlDev => sys::FS_IOCTL_DEV,
491            Access::ResolveUnix => sys::FS_RESOLVE_UNIX,
492            Access::Read => FS_READ_BITS,
493            Access::Write => FS_WRITE_BITS,
494            Access::ReadWrite => FS_READ_BITS | FS_WRITE_BITS,
495            Access::All => FS_ALL_BITS,
496        }
497    }
498}
499
500impl fmt::Display for Access {
501    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502        let name = match self {
503            Access::Execute => "execute",
504            Access::WriteFile => "write-file",
505            Access::ReadFile => "read-file",
506            Access::ReadDir => "read-dir",
507            Access::RemoveDir => "remove-dir",
508            Access::RemoveFile => "remove-file",
509            Access::MakeChar => "make-char",
510            Access::MakeDir => "make-dir",
511            Access::MakeReg => "make-reg",
512            Access::MakeSock => "make-sock",
513            Access::MakeFifo => "make-fifo",
514            Access::MakeBlock => "make-block",
515            Access::MakeSym => "make-sym",
516            Access::Refer => "refer",
517            Access::Truncate => "truncate",
518            Access::IoctlDev => "ioctl-dev",
519            Access::ResolveUnix => "resolve-unix",
520            Access::Read => "read",
521            Access::Write => "write",
522            Access::ReadWrite => "read-write",
523            Access::All => "all",
524        };
525        f.write_str(name)
526    }
527}
528
529/// A network access right.
530#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
531#[non_exhaustive]
532pub enum NetworkAccess {
533    /// Bind a TCP socket to a local port.
534    BindTcp,
535    /// Connect a TCP socket to a remote port.
536    ConnectTcp,
537    /// Bind a UDP socket to a local port.
538    BindUdp,
539    /// Set the remote port of a UDP socket, or send datagrams to it.
540    ConnectSendUdp,
541    /// [`NetworkAccess::BindTcp`] and [`NetworkAccess::ConnectTcp`].
542    Tcp,
543    /// [`NetworkAccess::BindUdp`] and [`NetworkAccess::ConnectSendUdp`].
544    Udp,
545    /// Every network access right.
546    All,
547}
548
549impl NetworkAccess {
550    const fn bits(self) -> u32 {
551        match self {
552            NetworkAccess::BindTcp => sys::NET_BIND_TCP,
553            NetworkAccess::ConnectTcp => sys::NET_CONNECT_TCP,
554            NetworkAccess::BindUdp => sys::NET_BIND_UDP,
555            NetworkAccess::ConnectSendUdp => sys::NET_CONNECT_SEND_UDP,
556            NetworkAccess::Tcp => NET_TCP_BITS,
557            NetworkAccess::Udp => NET_UDP_BITS,
558            NetworkAccess::All => NET_ALL_BITS,
559        }
560    }
561}
562
563impl fmt::Display for NetworkAccess {
564    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
565        let name = match self {
566            NetworkAccess::BindTcp => "bind-tcp",
567            NetworkAccess::ConnectTcp => "connect-tcp",
568            NetworkAccess::BindUdp => "bind-udp",
569            NetworkAccess::ConnectSendUdp => "connect-send-udp",
570            NetworkAccess::Tcp => "tcp",
571            NetworkAccess::Udp => "udp",
572            NetworkAccess::All => "all",
573        };
574        f.write_str(name)
575    }
576}
577
578/// An IPC scope, restricting interactions with processes outside the sandbox.
579///
580/// Scopes are active by default; [`Ruleset::allow_scope`] lifts them.
581#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
582#[non_exhaustive]
583pub enum Scope {
584    /// Connecting to abstract UNIX sockets created outside the sandbox.
585    AbstractUnixSocket,
586    /// Sending signals to processes outside the sandbox.
587    Signal,
588    /// Every scope.
589    All,
590}
591
592impl Scope {
593    const fn bits(self) -> u32 {
594        match self {
595            Scope::AbstractUnixSocket => sys::SCOPE_ABSTRACT_UNIX_SOCKET,
596            Scope::Signal => sys::SCOPE_SIGNAL,
597            Scope::All => SCOPE_ALL_BITS,
598        }
599    }
600}
601
602impl fmt::Display for Scope {
603    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
604        let name = match self {
605            Scope::AbstractUnixSocket => "abstract-unix-socket",
606            Scope::Signal => "signal",
607            Scope::All => "all",
608        };
609        f.write_str(name)
610    }
611}
612
613/// Why a path cannot be used for a rule.
614#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
615#[non_exhaustive]
616pub enum PathError {
617    /// The path contains a NUL byte; the kernel would silently read a
618    /// truncated path from it.
619    ContainsNul,
620    /// The path, including its terminating NUL byte, does not fit in
621    /// `PATH_MAX` (4096) bytes.
622    TooLong,
623}
624
625impl fmt::Display for PathError {
626    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
627        f.write_str(match self {
628            PathError::ContainsNul => "it contains a NUL byte",
629            PathError::TooLong => "it is longer than PATH_MAX bytes (4096)",
630        })
631    }
632}
633
634/// A feature (access right or scope) that the running kernel cannot enforce.
635#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
636#[non_exhaustive]
637pub enum Feature {
638    /// A filesystem access right.
639    FileSystem(Access),
640    /// A network access right.
641    Network(NetworkAccess),
642    /// An IPC scope.
643    Scope(Scope),
644}
645
646impl fmt::Display for Feature {
647    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
648        match self {
649            Feature::FileSystem(access) => write!(f, "access right `{access}`"),
650            Feature::Network(access) => write!(f, "network access `{access}`"),
651            Feature::Scope(scope) => write!(f, "IPC scope `{scope}`"),
652        }
653    }
654}
655
656/// A Landlock operation that failed with an unexpected error.
657#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
658#[non_exhaustive]
659pub enum Operation {
660    /// `landlock_create_ruleset(2)`.
661    CreateRuleset,
662    /// `landlock_add_rule(2)`.
663    AddRule,
664    /// `landlock_restrict_self(2)`.
665    RestrictSelf,
666    /// `prctl(PR_SET_NO_NEW_PRIVS)`.
667    SetNoNewPrivs,
668    /// `fstat(2)`, used to tell files from directories.
669    Stat,
670}
671
672impl fmt::Display for Operation {
673    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
674        f.write_str(match self {
675            Operation::CreateRuleset => "landlock_create_ruleset",
676            Operation::AddRule => "landlock_add_rule",
677            Operation::RestrictSelf => "landlock_restrict_self",
678            Operation::SetNoNewPrivs => "prctl(PR_SET_NO_NEW_PRIVS)",
679            Operation::Stat => "fstat",
680        })
681    }
682}
683
684/// Everything that can prevent a policy from being enforced.
685///
686/// [`Ruleset::restrict_self`] returns the first error encountered while the
687/// policy was built. When it returns an error, no restriction has been applied
688/// (with one caveat: a failure while enforcing the second or third ruleset
689/// layer can leave the process partially restricted, see [`Error::Syscall`]).
690#[derive(Debug)]
691#[non_exhaustive]
692pub enum Error {
693    /// Landlock is not available: the kernel does not implement it (`ENOSYS`),
694    /// it is disabled at boot time (`EOPNOTSUPP`, `lsm=` kernel command line),
695    /// or the system calls are blocked (for example by a seccomp filter).
696    NotSupported {
697        /// The error reported by the kernel.
698        source: io::Error,
699    },
700    /// In strict mode, the running kernel cannot enforce a requested feature.
701    Unsupported {
702        /// The feature that cannot be enforced.
703        feature: Feature,
704        /// The ABI version supported by the running kernel.
705        kernel_abi: u32,
706        /// The oldest ABI version that supports `feature`.
707        required_abi: u32,
708    },
709    /// The path is not usable for a rule.
710    InvalidPath {
711        /// The offending path.
712        path: PathBuf,
713        /// What is wrong with it.
714        reason: PathError,
715    },
716    /// The path could not be opened.
717    Open {
718        /// The offending path.
719        path: PathBuf,
720        /// The error reported by the kernel (`ENOENT`, `EACCES`, `ELOOP`, …).
721        source: io::Error,
722    },
723    /// The requested access has no meaning for the object (for example
724    /// `Access::ReadDir` on a regular file).
725    InapplicableAccess {
726        /// The requested access.
727        access: Access,
728        /// The path, when the rule was created from a path.
729        path: Option<PathBuf>,
730    },
731    /// The file descriptor does not identify a file hierarchy that Landlock can
732    /// restrict: it refers to a pipe, a socket, an anonymous inode, a ruleset,
733    /// or an internal filesystem such as `/proc/<pid>/ns/*`.
734    InvalidFileDescriptor {
735        /// The error reported by the kernel.
736        source: io::Error,
737    },
738    /// The kernel's limit of 16 stacked rulesets is reached for this thread.
739    ///
740    /// A single [`Ruleset`] applies at most three layers (filesystem, network
741    /// and scope). This error means the thread already inherited or applied
742    /// many rulesets; build a single policy per thread where possible.
743    LayerLimitReached,
744    /// An unexpected error from a Landlock system call.
745    Syscall {
746        /// The operation that failed.
747        operation: Operation,
748        /// The error reported by the kernel.
749        source: io::Error,
750    },
751}
752
753impl fmt::Display for Error {
754    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
755        match self {
756            Error::NotSupported {
757                source,
758            } => {
759                write!(f, "Landlock is not supported by this kernel: {source}")
760            }
761            Error::Unsupported {
762                feature,
763                kernel_abi,
764                required_abi,
765            } => {
766                write!(
767                    f,
768                    "the running kernel cannot enforce {feature}: it needs Landlock ABI {}",
769                    required_abi
770                )?;
771                if let Some(version) = abi_kernel_version(*required_abi) {
772                    write!(f, " (Linux {version} or later)")?;
773                }
774                write!(f, ", but only ABI {kernel_abi} is available")
775            }
776            Error::InvalidPath {
777                path,
778                reason,
779            } => {
780                write!(f, "invalid path {path:?}: {reason}")
781            }
782            Error::Open {
783                path,
784                source,
785            } => {
786                write!(f, "failed to open {path:?}: {source}")
787            }
788            Error::InapplicableAccess {
789                access,
790                path,
791            } => match path {
792                Some(path) => write!(
793                    f,
794                    "access right `{access}` does not apply to {path:?} (or it grants nothing there)"
795                ),
796                None => write!(f, "access right `{access}` does not apply to the given file descriptor"),
797            },
798            Error::InvalidFileDescriptor {
799                source,
800            } => {
801                write!(f, "the file descriptor does not identify a file hierarchy: {source}")
802            }
803            Error::LayerLimitReached => {
804                write!(f, "the kernel's limit of 16 stacked rulesets is reached for this thread")
805            }
806            Error::Syscall {
807                operation,
808                source,
809            } => {
810                write!(f, "the {operation} system call failed: {source}")
811            }
812        }
813    }
814}
815
816impl StdError for Error {
817    fn source(&self) -> Option<&(dyn StdError + 'static)> {
818        match self {
819            Error::NotSupported {
820                source,
821            }
822            | Error::Open {
823                source, ..
824            }
825            | Error::InvalidFileDescriptor {
826                source,
827            }
828            | Error::Syscall {
829                source, ..
830            } => Some(source),
831            _ => None,
832        }
833    }
834}
835
836/// Whether a ruleset enforces every requested right or adapts to the kernel.
837#[derive(Clone, Copy, Debug, Eq, PartialEq)]
838enum Mode {
839    /// Fail when the kernel cannot enforce a requested right.
840    Strict,
841    /// Drop rights the kernel cannot enforce.
842    BestEffort,
843}
844
845/// A Landlock policy under construction.
846///
847/// Rules are added with the `allow_*` methods and the policy is applied with
848/// [`Ruleset::restrict_self`], which consumes the builder. Building never
849/// fails: errors (invalid paths, unsupported rights, …) are remembered and
850/// reported by `restrict_self`, and nothing is enforced when it returns them.
851///
852/// The builder is [`Send`] and [`Sync`], and every method takes `self` by
853/// value, so a policy is never shared or mutated by two threads at once.
854pub struct Ruleset {
855    mode: Mode,
856    abi: u32,
857    handled_fs: u64,
858    fs: Option<OwnedFd>,
859    net: Option<OwnedFd>,
860    scoped: u32,
861    error: Option<Error>,
862}
863
864impl fmt::Debug for Ruleset {
865    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
866        f.debug_struct("Ruleset")
867            .field("mode", &self.mode)
868            .field("abi", &self.abi)
869            .field("handled_access_fs", &format_args!("{:#x}", self.handled_fs))
870            .field("scoped", &format_args!("{:#x}", self.scoped))
871            .field("failed", &self.error.is_some())
872            .finish()
873    }
874}
875
876/// Creates a policy that fails if the running kernel cannot enforce a rule.
877///
878/// Rules that need a newer Landlock ABI (see the [kernel support
879/// table](index.html#kernel-support)) make [`Ruleset::restrict_self`] return
880/// [`Error::Unsupported`]. Use [`new_best_effort`] to degrade instead.
881#[must_use]
882pub fn new_strict() -> Ruleset {
883    Ruleset::new(Mode::Strict)
884}
885
886/// Creates a policy that drops the rights the running kernel cannot enforce.
887///
888/// If a rule ends up granting nothing (for example `Access::ResolveUnix` on a
889/// kernel older than Landlock ABI 9), the rule is skipped, which leaves the
890/// affected path denied. See [`new_strict`] for the alternative.
891#[must_use]
892pub fn new_best_effort() -> Ruleset {
893    Ruleset::new(Mode::BestEffort)
894}
895
896/// The Landlock ABI version supported by the running kernel.
897///
898/// # Errors
899///
900/// Returns [`Error::NotSupported`] when Landlock is not available. This is the
901/// same condition as [`is_supported`] returning `false`.
902pub fn abi_version() -> Result<u32, Error> {
903    sys::abi_version().map_err(|source| Error::NotSupported {
904        source,
905    })
906}
907
908/// Whether the running kernel supports Landlock at all.
909#[must_use]
910pub fn is_supported() -> bool {
911    abi_version().is_ok()
912}
913
914impl Ruleset {
915    fn new(mode: Mode) -> Ruleset {
916        match sys::abi_version() {
917            Ok(abi) if abi >= 1 => Ruleset::with_abi(mode, abi),
918            Ok(_) => Ruleset::unavailable(Error::NotSupported {
919                source: io::Error::new(io::ErrorKind::Unsupported, "Landlock reported ABI version 0"),
920            }),
921            Err(source) => Ruleset::unavailable(Error::NotSupported {
922                source,
923            }),
924        }
925    }
926
927    /// A ruleset whose policy is decided against Landlock ABI `abi`, used to
928    /// simulate older kernels. `abi` must not exceed the running kernel's, or
929    /// creating the rulesets fails.
930    fn with_abi(mode: Mode, abi: u32) -> Ruleset {
931        let handled_fs = FS_ALL_BITS & fs_supported(abi);
932        let mut ruleset = Ruleset {
933            mode,
934            abi,
935            handled_fs,
936            fs: None,
937            net: None,
938            scoped: SCOPE_ALL_BITS & scope_supported(abi),
939            error: None,
940        };
941
942        // These rulesets are always created, even without rules: everything is
943        // denied by default.
944        ruleset.fs = match sys::create_ruleset(handled_fs, 0, 0) {
945            Ok(fd) => Some(fd),
946            Err(source) => {
947                ruleset.error = Some(Error::Syscall {
948                    operation: Operation::CreateRuleset,
949                    source,
950                });
951                return ruleset;
952            }
953        };
954
955        let handled_net = NET_ALL_BITS & net_supported(abi);
956        if handled_net != 0 {
957            ruleset.net = match sys::create_ruleset(0, handled_net, 0) {
958                Ok(fd) => Some(fd),
959                Err(source) => {
960                    ruleset.error = Some(Error::Syscall {
961                        operation: Operation::CreateRuleset,
962                        source,
963                    });
964                    None
965                }
966            };
967        }
968
969        ruleset
970    }
971
972    fn unavailable(error: Error) -> Ruleset {
973        Ruleset {
974            mode: Mode::BestEffort,
975            abi: 0,
976            handled_fs: 0,
977            fs: None,
978            net: None,
979            scoped: 0,
980            error: Some(error),
981        }
982    }
983
984    fn fail(&mut self, error: Error) {
985        if self.error.is_none() {
986            self.error = Some(error);
987        }
988    }
989
990    /// The number of access rights handled by the filesystem ruleset.
991    fn fs_fd(&self) -> Option<BorrowedFd<'_>> {
992        self.fs.as_ref().map(OwnedFd::as_fd)
993    }
994
995    fn net_fd(&self) -> Option<BorrowedFd<'_>> {
996        self.net.as_ref().map(OwnedFd::as_fd)
997    }
998
999    /// Grants `access` on each path of `paths`.
1000    ///
1001    /// The same access rights are granted on every path. Paths are opened with
1002    /// `O_PATH`, which requires no permission on the target, and rules are
1003    /// attached to the objects the paths resolve to (symlinks are followed).
1004    ///
1005    /// Rights that do not apply to the object are dropped (for example
1006    /// [`Access::RemoveFile`] on a regular file). Requesting access that grants
1007    /// nothing on the object (for example [`Access::ReadDir`] on a regular
1008    /// file) is an invalid rule and is reported by [`Ruleset::restrict_self`].
1009    ///
1010    /// Failure modes are deferred to [`Ruleset::restrict_self`]; this method
1011    /// never returns an error.
1012    #[must_use]
1013    pub fn allow_filesystem(mut self, access: Access, paths: impl IntoIterator<Item = impl AsRef<Path>>) -> Self {
1014        for path in paths {
1015            self.add_filesystem_path(access, path.as_ref());
1016        }
1017        self
1018    }
1019
1020    /// Grants `access` on each of `fds`, which must identify file hierarchies.
1021    ///
1022    /// This is the equivalent of [`Ruleset::allow_filesystem`] for callers that
1023    /// want to control how the path is resolved (for example with `openat2` and
1024    /// `RESOLVE_NO_SYMLINKS`). The descriptors may be closed once
1025    /// [`Ruleset::restrict_self`] returned: the kernel keeps a reference to the
1026    /// underlying objects.
1027    ///
1028    /// Failure modes are deferred to [`Ruleset::restrict_self`]; this method
1029    /// never returns an error.
1030    #[must_use]
1031    pub fn allow_file_descriptor(mut self, access: Access, fds: impl IntoIterator<Item = impl AsFd>) -> Self {
1032        for fd in fds {
1033            self.add_filesystem_fd(access, fd.as_fd(), None);
1034        }
1035        self
1036    }
1037
1038    /// Grants `access` on each of `ports`.
1039    ///
1040    /// Port `0` covers the ephemeral port range for binding. Landlock has no
1041    /// wildcard port, so every port has to be listed. Only TCP and UDP are
1042    /// restricted.
1043    ///
1044    /// Failure modes are deferred to [`Ruleset::restrict_self`]; this method
1045    /// never returns an error.
1046    #[must_use]
1047    pub fn allow_network(mut self, access: NetworkAccess, ports: impl IntoIterator<Item = u16>) -> Self {
1048        self.add_network(access, ports.into_iter());
1049        self
1050    }
1051
1052    /// Lifts the IPC restriction described by `scope`.
1053    ///
1054    /// Scopes are active by default. Unlike filesystem and network access,
1055    /// Landlock cannot grant exceptions to a scope: it can only be lifted as a
1056    /// whole.
1057    ///
1058    /// Failure modes are deferred to [`Ruleset::restrict_self`]; this method
1059    /// never returns an error.
1060    #[must_use]
1061    pub fn allow_scope(mut self, scope: Scope) -> Self {
1062        self.scoped &= !scope.bits();
1063        self
1064    }
1065
1066    fn add_filesystem_path(&mut self, access: Access, path: &Path) {
1067        if self.error.is_some() || self.fs.is_none() {
1068            return;
1069        }
1070
1071        use std::os::unix::ffi::OsStrExt;
1072        let bytes = path.as_os_str().as_bytes();
1073
1074        if bytes.contains(&0) {
1075            self.fail(Error::InvalidPath {
1076                path: path.to_path_buf(),
1077                reason: PathError::ContainsNul,
1078            });
1079            return;
1080        }
1081        if bytes.len() >= PATH_MAX {
1082            self.fail(Error::InvalidPath {
1083                path: path.to_path_buf(),
1084                reason: PathError::TooLong,
1085            });
1086            return;
1087        }
1088
1089        let mut buffer = [0u8; PATH_MAX];
1090        buffer[..bytes.len()].copy_from_slice(bytes);
1091
1092        let fd = match sys::open_path(&buffer[..=bytes.len()]) {
1093            Ok(fd) => fd,
1094            Err(source) => {
1095                self.fail(Error::Open {
1096                    path: path.to_path_buf(),
1097                    source,
1098                });
1099                return;
1100            }
1101        };
1102        self.add_filesystem_fd(access, fd.as_fd(), Some(path));
1103    }
1104
1105    fn add_filesystem_fd(&mut self, access: Access, fd: BorrowedFd<'_>, path: Option<&Path>) {
1106        let Some(ruleset_fd) = self.fs_fd() else {
1107            return;
1108        };
1109
1110        let is_dir = match sys::fd_is_dir(fd.as_raw_fd()) {
1111            Ok(is_dir) => is_dir,
1112            Err(source) => {
1113                self.fail(Error::Syscall {
1114                    operation: Operation::Stat,
1115                    source,
1116                });
1117                return;
1118            }
1119        };
1120
1121        // Rights that do not apply to the object are dropped: the kernel
1122        // rejects a rule that grants directory-only rights on a file.
1123        let applicable = if is_dir {
1124            access.bits()
1125        } else {
1126            access.bits() & FS_FILE_BITS
1127        };
1128        if applicable == 0 {
1129            self.fail(Error::InapplicableAccess {
1130                access,
1131                path: path.map(Path::to_path_buf),
1132            });
1133            return;
1134        }
1135
1136        let supported = fs_supported(self.abi);
1137        let allowed = match self.mode {
1138            Mode::Strict => {
1139                if applicable & !supported != 0 {
1140                    self.fail(Error::Unsupported {
1141                        feature: Feature::FileSystem(access),
1142                        kernel_abi: self.abi,
1143                        required_abi: min_abi_fs(applicable),
1144                    });
1145                    return;
1146                }
1147                applicable
1148            }
1149            Mode::BestEffort => {
1150                let trimmed = applicable & supported;
1151                if trimmed == 0 {
1152                    return;
1153                }
1154                trimmed
1155            }
1156        };
1157
1158        if let Err(source) = sys::add_path_rule(ruleset_fd.as_raw_fd(), allowed, fd.as_raw_fd()) {
1159            match source.raw_os_error() {
1160                Some(EBADFD) => self.fail(Error::InvalidFileDescriptor {
1161                    source,
1162                }),
1163                // The kernel only rejects a path rule with EINVAL when the
1164                // access rights do not apply to the object.
1165                Some(libc::EINVAL) => self.fail(Error::InapplicableAccess {
1166                    access,
1167                    path: path.map(Path::to_path_buf),
1168                }),
1169                _ => self.fail(Error::Syscall {
1170                    operation: Operation::AddRule,
1171                    source,
1172                }),
1173            }
1174        }
1175    }
1176
1177    fn add_network(&mut self, access: NetworkAccess, ports: impl Iterator<Item = u16>) {
1178        if self.error.is_some() {
1179            return;
1180        }
1181
1182        let supported = net_supported(self.abi);
1183        let allowed = match self.mode {
1184            Mode::Strict => {
1185                if access.bits() & !supported != 0 {
1186                    self.fail(Error::Unsupported {
1187                        feature: Feature::Network(access),
1188                        kernel_abi: self.abi,
1189                        required_abi: min_abi_net(access.bits()),
1190                    });
1191                    return;
1192                }
1193                access.bits()
1194            }
1195            Mode::BestEffort => {
1196                let trimmed = access.bits() & supported;
1197                if trimmed == 0 {
1198                    return;
1199                }
1200                trimmed
1201            }
1202        };
1203
1204        let Some(ruleset_fd) = self.net_fd().map(|fd| fd.as_raw_fd()) else {
1205            return;
1206        };
1207
1208        for port in ports {
1209            if let Err(source) = sys::add_net_rule(ruleset_fd, allowed, port) {
1210                match source.raw_os_error() {
1211                    Some(libc::EAFNOSUPPORT) => {
1212                        // The kernel has no TCP/IP support; the rule cannot be
1213                        // enforced, and there is nothing to restrict.
1214                        if self.mode == Mode::Strict {
1215                            self.fail(Error::Syscall {
1216                                operation: Operation::AddRule,
1217                                source,
1218                            });
1219                        }
1220                        return;
1221                    }
1222                    _ => self.fail(Error::Syscall {
1223                        operation: Operation::AddRule,
1224                        source,
1225                    }),
1226                }
1227            }
1228        }
1229    }
1230
1231    /// Applies the policy to the calling thread (and, on Landlock ABI 8 and
1232    /// newer, to every thread of the process).
1233    ///
1234    /// This consumes the ruleset, which is final: restrictions can never be
1235    /// relaxed afterwards, only added to. On success, the returned ABI version
1236    /// is the version reported by the running kernel, and the restrictions stay
1237    /// in place when the builder is dropped.
1238    ///
1239    /// `PR_SET_NO_NEW_PRIVS` is set on the calling thread first (on Landlock ABI
1240    /// 11 and newer the kernel does it as part of the enforcement). It is never
1241    /// unset, and it prevents the process from gaining privileges through
1242    /// set-user-ID programs.
1243    ///
1244    /// # Errors
1245    ///
1246    /// Returns the first error encountered while building the policy (see
1247    /// [`Error`]) without applying it, or an error reported by the kernel.
1248    pub fn restrict_self(self) -> Result<u32, Error> {
1249        if let Some(error) = self.error {
1250            return Err(error);
1251        }
1252
1253        let (flags, atomic_no_new_privs) = restrict_flags(self.abi);
1254        if !atomic_no_new_privs {
1255            sys::set_no_new_privs().map_err(|source| Error::Syscall {
1256                operation: Operation::SetNoNewPrivs,
1257                source,
1258            })?;
1259        }
1260
1261        // The scope restrictions have no rules, so this ruleset is created last
1262        // and only when a scope is still active.
1263        if self.scoped != 0 {
1264            let scoped = self.scoped & scope_supported(self.abi);
1265            if scoped != 0 {
1266                match sys::create_ruleset(0, 0, scoped) {
1267                    Ok(fd) => enforce(&fd, flags)?,
1268                    Err(source) => {
1269                        return Err(Error::Syscall {
1270                            operation: Operation::CreateRuleset,
1271                            source,
1272                        });
1273                    }
1274                }
1275            }
1276        }
1277
1278        for fd in [self.fs.as_ref(), self.net.as_ref()].into_iter().flatten() {
1279            enforce(fd, flags)?;
1280        }
1281
1282        Ok(self.abi)
1283    }
1284}
1285
1286/// The `landlock_restrict_self(2)` flags to use on a kernel reporting `abi`,
1287/// and whether the kernel sets `no_new_privs` itself. Landlock ABI 8 added the
1288/// `TSYNC` flag (process-wide enforcement) and ABI 11 added the atomic
1289/// `no_new_privs` flag.
1290const fn restrict_flags(abi: u32) -> (u32, bool) {
1291    let mut flags = 0u32;
1292    if abi >= 8 {
1293        // Applies the policy to every thread of the process.
1294        flags |= sys::LANDLOCK_RESTRICT_SELF_TSYNC;
1295    }
1296    let atomic_no_new_privs = abi >= 11;
1297    if atomic_no_new_privs {
1298        flags |= sys::LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
1299    }
1300    (flags, atomic_no_new_privs)
1301}
1302
1303/// Applies one ruleset layer, mapping the kernel's errors.
1304fn enforce(fd: &OwnedFd, flags: u32) -> Result<(), Error> {
1305    match sys::restrict_self(fd.as_raw_fd(), flags) {
1306        Ok(()) => Ok(()),
1307        Err(source) => match source.raw_os_error() {
1308            Some(libc::E2BIG) => Err(Error::LayerLimitReached),
1309            _ => Err(Error::Syscall {
1310                operation: Operation::RestrictSelf,
1311                source,
1312            }),
1313        },
1314    }
1315}
1316
1317#[cfg(test)]
1318mod tests {
1319    use std::{ffi::CStr, os::unix::ffi::OsStrExt};
1320
1321    use super::*;
1322
1323    /// Runs `f` in a forked child and returns its exit status. The closure must
1324    /// not allocate (see the integration tests for the rationale).
1325    fn in_child(f: impl FnOnce() -> i32) -> i32 {
1326        let pid = unsafe { libc::fork() };
1327        assert!(pid >= 0, "fork() failed");
1328        if pid == 0 {
1329            let code = f();
1330            unsafe { libc::_exit(code) };
1331        }
1332        let mut status = 0;
1333        let waited = unsafe { libc::waitpid(pid, &mut status, 0) };
1334        assert_eq!(waited, pid, "waitpid() failed");
1335        if libc::WIFEXITED(status) {
1336            libc::WEXITSTATUS(status)
1337        } else {
1338            -1
1339        }
1340    }
1341
1342    fn real_abi() -> Option<u32> {
1343        abi_version().ok()
1344    }
1345
1346    fn open_errno(path: &CStr, flags: libc::c_int) -> i32 {
1347        let fd = unsafe { libc::open(path.as_ptr(), flags) };
1348        if fd < 0 {
1349            return std::io::Error::last_os_error().raw_os_error().unwrap_or(-1);
1350        }
1351        unsafe { libc::close(fd) };
1352        0
1353    }
1354
1355    #[test]
1356    fn groups_expand_to_the_documented_rights() {
1357        assert_eq!(Access::Read.bits(), sys::FS_READ_FILE | sys::FS_READ_DIR);
1358        assert_eq!(Access::Write.bits(), FS_WRITE_BITS);
1359        assert_eq!(Access::ReadWrite.bits(), FS_READ_BITS | FS_WRITE_BITS);
1360        assert_eq!(Access::All.bits(), FS_ALL_BITS);
1361
1362        // `Write` covers the rights that make writing work, including
1363        // truncation and reparenting.
1364        assert_eq!(Access::Write.bits() & sys::FS_TRUNCATE, sys::FS_TRUNCATE);
1365        assert_eq!(Access::Write.bits() & sys::FS_REFER, sys::FS_REFER);
1366        assert_eq!(Access::Write.bits() & sys::FS_WRITE_FILE, sys::FS_WRITE_FILE);
1367        // Device ioctls and UNIX socket lookups are not part of `ReadWrite`.
1368        assert_eq!(Access::ReadWrite.bits() & sys::FS_IOCTL_DEV, 0);
1369        assert_eq!(Access::ReadWrite.bits() & sys::FS_RESOLVE_UNIX, 0);
1370        assert_eq!(
1371            Access::All.bits() & sys::FS_IOCTL_DEV,
1372            sys::FS_IOCTL_DEV,
1373            "`All` includes device ioctls"
1374        );
1375        assert_eq!(
1376            Access::All.bits() & sys::FS_RESOLVE_UNIX,
1377            sys::FS_RESOLVE_UNIX,
1378            "`All` includes UNIX socket lookups"
1379        );
1380
1381        assert_eq!(NetworkAccess::Tcp.bits(), NET_TCP_BITS);
1382        assert_eq!(NetworkAccess::Udp.bits(), NET_UDP_BITS);
1383        assert_eq!(NetworkAccess::All.bits(), NET_ALL_BITS);
1384        assert_eq!(Scope::All.bits(), SCOPE_ALL_BITS);
1385    }
1386
1387    #[test]
1388    fn the_abi_tables_cover_every_bit_exactly_once() {
1389        let mut covered = 0u64;
1390        for (bit, introduced) in FS_ABI_TABLE {
1391            assert!(introduced >= 1);
1392            assert_eq!(covered & bit, 0, "bit {bit:#x} is listed twice");
1393            covered |= bit;
1394        }
1395        assert_eq!(covered, FS_ALL_BITS, "a filesystem right is missing");
1396
1397        let mut covered = 0u32;
1398        for (bit, introduced) in NET_ABI_TABLE {
1399            assert!(introduced >= 1);
1400            assert_eq!(covered & bit, 0, "bit {bit:#x} is listed twice");
1401            covered |= bit;
1402        }
1403        assert_eq!(covered, NET_ALL_BITS, "a network right is missing");
1404
1405        let mut covered = 0u32;
1406        for (bit, introduced) in SCOPE_ABI_TABLE {
1407            assert!(introduced >= 1);
1408            assert_eq!(covered & bit, 0, "bit {bit:#x} is listed twice");
1409            covered |= bit;
1410        }
1411        assert_eq!(covered, SCOPE_ALL_BITS, "a scope is missing");
1412    }
1413
1414    #[test]
1415    fn the_abi_tables_match_the_documented_kernels() {
1416        assert_eq!(fs_supported(0), 0);
1417        assert_eq!(fs_supported(1), 0x1FFF, "Linux 5.13: rights 0 to 12");
1418        assert_eq!(fs_supported(2), 0x3FFF, "Linux 5.19: adds refer");
1419        assert_eq!(fs_supported(3), 0x7FFF, "Linux 6.2: adds truncate");
1420        assert_eq!(fs_supported(4), 0x7FFF, "Linux 6.7: network only");
1421        assert_eq!(fs_supported(5), 0xFFFF, "Linux 6.10: adds ioctl-dev");
1422        assert_eq!(fs_supported(8), 0xFFFF, "Linux 7.0: no new filesystem right");
1423        assert_eq!(fs_supported(9), 0x1FFFF, "Linux 7.1: adds resolve-unix");
1424        assert_eq!(fs_supported(100), FS_ALL_BITS, "unknown rights are never sent");
1425
1426        assert_eq!(net_supported(3), 0);
1427        assert_eq!(net_supported(4), 0b0011, "Linux 6.7: TCP");
1428        assert_eq!(net_supported(9), 0b0011);
1429        assert_eq!(net_supported(10), 0b1111, "Linux 7.2: UDP");
1430
1431        assert_eq!(scope_supported(5), 0);
1432        assert_eq!(scope_supported(6), 0b11, "Linux 6.12: both scopes");
1433
1434        assert_eq!(abi_kernel_version(1), Some("5.13"));
1435        assert_eq!(abi_kernel_version(2), Some("5.19"));
1436        assert_eq!(abi_kernel_version(8), Some("7.0"));
1437        assert_eq!(abi_kernel_version(12), None);
1438    }
1439
1440    #[test]
1441    fn the_minimum_abi_of_every_group_is_the_documented_one() {
1442        assert_eq!(min_abi_fs(0), 0);
1443        assert_eq!(min_abi_fs(Access::Read.bits()), 1);
1444        assert_eq!(min_abi_fs(Access::Execute.bits()), 1);
1445        assert_eq!(min_abi_fs(Access::Refer.bits()), 2);
1446        assert_eq!(min_abi_fs(Access::Write.bits()), 3);
1447        assert_eq!(min_abi_fs(Access::ReadWrite.bits()), 3);
1448        assert_eq!(min_abi_fs(Access::IoctlDev.bits()), 5);
1449        assert_eq!(min_abi_fs(Access::All.bits()), 9);
1450        assert_eq!(min_abi_fs(Access::ResolveUnix.bits()), 9);
1451        assert_eq!(min_abi_net(NetworkAccess::Tcp.bits()), 4);
1452        assert_eq!(min_abi_net(NetworkAccess::Udp.bits()), 10);
1453    }
1454
1455    #[test]
1456    fn unsupported_errors_name_the_required_abi_and_kernel() {
1457        let error = Error::Unsupported {
1458            feature: Feature::FileSystem(Access::All),
1459            kernel_abi: 2,
1460            required_abi: 9,
1461        };
1462        let message = error.to_string();
1463        assert!(message.contains("access right `all`"), "{message}");
1464        assert!(message.contains("ABI 9"), "{message}");
1465        assert!(message.contains("Linux 7.1"), "{message}");
1466        assert!(message.contains("ABI 2"), "{message}");
1467        assert!(std::error::Error::source(&error).is_none());
1468    }
1469
1470    #[test]
1471    fn restrict_flags_follow_the_abi() {
1472        // Before Landlock ABI 8 there is no process-wide enforcement, and
1473        // `no_new_privs` is always set by this crate.
1474        assert_eq!(restrict_flags(1), (0, false));
1475        assert_eq!(restrict_flags(7), (0, false));
1476        // ABI 8: `TSYNC` for process-wide enforcement.
1477        assert_eq!(restrict_flags(8), (sys::LANDLOCK_RESTRICT_SELF_TSYNC, false));
1478        assert_eq!(restrict_flags(10), (sys::LANDLOCK_RESTRICT_SELF_TSYNC, false));
1479        // ABI 11: the kernel sets `no_new_privs` itself.
1480        let (flags, atomic) = restrict_flags(11);
1481        assert_eq!(
1482            flags,
1483            sys::LANDLOCK_RESTRICT_SELF_TSYNC | sys::LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
1484        );
1485        assert!(atomic);
1486        assert!(restrict_flags(100).1, "newer kernels keep the behaviour");
1487    }
1488
1489    #[test]
1490    fn strict_mode_rejects_rights_an_older_kernel_cannot_enforce() {
1491        let Some(real) = real_abi() else {
1492            return;
1493        };
1494        if real < 2 {
1495            return;
1496        }
1497        // Simulate Linux 6.0 (Landlock ABI 2): truncation needs ABI 3.
1498        let error = Ruleset::with_abi(Mode::Strict, 2)
1499            .allow_filesystem(Access::Write, ["/tmp"])
1500            .restrict_self()
1501            .unwrap_err();
1502        match error {
1503            Error::Unsupported {
1504                kernel_abi,
1505                required_abi,
1506                ..
1507            } => {
1508                assert_eq!(kernel_abi, 2);
1509                assert_eq!(required_abi, 3);
1510            }
1511            other => panic!("unexpected error: {other}"),
1512        }
1513
1514        // Rights known to ABI 2 are still accepted.
1515        let code = in_child(|| {
1516            match Ruleset::with_abi(Mode::Strict, 2)
1517                .allow_filesystem(Access::Read, ["/tmp"])
1518                .restrict_self()
1519            {
1520                Ok(abi) => i32::from(abi != 2),
1521                Err(_) => 1,
1522            }
1523        });
1524        assert_eq!(code, 0);
1525    }
1526
1527    #[test]
1528    fn best_effort_trims_rights_an_older_kernel_cannot_enforce() {
1529        let Some(real) = real_abi() else {
1530            return;
1531        };
1532        if real < 2 {
1533            return;
1534        }
1535        let code = in_child(|| {
1536            match Ruleset::with_abi(Mode::BestEffort, 2)
1537                .allow_filesystem(Access::Write, ["/tmp"])
1538                .restrict_self()
1539            {
1540                Ok(abi) => i32::from(abi != 2),
1541                Err(_) => 1,
1542            }
1543        });
1544        assert_eq!(code, 0);
1545    }
1546
1547    /// End-to-end check of the ABI masking: a policy built against Landlock
1548    /// ABI 2 cannot restrict truncation, while the same policy built against
1549    /// the running kernel's ABI can.
1550    #[test]
1551    fn older_kernels_cannot_restrict_truncation() {
1552        let Some(real) = real_abi() else {
1553            return;
1554        };
1555        if real < 3 {
1556            // The running kernel cannot restrict truncation either.
1557            return;
1558        }
1559
1560        let directory = std::env::temp_dir().join(format!("landlock-truncate-{}", std::process::id()));
1561        let _ = std::fs::remove_dir_all(&directory);
1562        std::fs::create_dir_all(&directory).unwrap();
1563        let file = directory.join("file");
1564        std::fs::write(&file, b"hello").unwrap();
1565        let c_directory = std::ffi::CString::new(directory.as_os_str().as_bytes()).unwrap();
1566        let c_file = std::ffi::CString::new(file.as_os_str().as_bytes()).unwrap();
1567        let c_file_again = c_file.clone();
1568
1569        let granted = directory.clone();
1570        // The running kernel knows about `Access::Truncate`, so a policy that
1571        // handles it without granting it makes `O_TRUNC` fail...
1572        let denied = in_child(move || {
1573            if Ruleset::with_abi(Mode::BestEffort, real)
1574                .allow_filesystem(Access::WriteFile, [granted])
1575                .restrict_self()
1576                .is_err()
1577            {
1578                return -1;
1579            }
1580            open_errno(&c_file, libc::O_WRONLY | libc::O_TRUNC)
1581        });
1582        assert_eq!(denied, libc::EACCES, "...but truncation must be denied");
1583
1584        let granted = directory.clone();
1585        // ...while the same policy on a kernel that only knows ABI 2 cannot
1586        // restrict truncation at all, so it is allowed everywhere.
1587        let allowed = in_child(move || {
1588            if Ruleset::with_abi(Mode::BestEffort, 2)
1589                .allow_filesystem(Access::WriteFile, [granted])
1590                .restrict_self()
1591                .is_err()
1592            {
1593                return -1;
1594            }
1595            open_errno(&c_file_again, libc::O_WRONLY | libc::O_TRUNC)
1596        });
1597        assert_eq!(allowed, 0, "a Linux 6.0 kernel cannot restrict truncation");
1598
1599        let _ = &c_directory;
1600        let _ = std::fs::remove_dir_all(&directory);
1601    }
1602}