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}