landlock/
lib.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2
3//! Landlock is a security feature available since Linux 5.13.
4//! The goal is to enable to restrict ambient rights
5//! (e.g., global filesystem access)
6//! for a set of processes by creating safe security sandboxes as new security layers
7//! in addition to the existing system-wide access-controls.
8//! This kind of sandbox is expected to help mitigate the security impact of bugs,
9//! unexpected or malicious behaviors in applications.
10//! Landlock empowers any process, including unprivileged ones, to securely restrict themselves.
11//! More information about Landlock can be found in the [official website](https://landlock.io).
12//!
13//! This crate provides a safe abstraction for the Landlock system calls, along with some helpers.
14//!
15//! Minimum Supported Rust Version (MSRV): 1.68
16//!
17//! # Use cases
18//!
19//! This crate is especially useful to protect users' data by sandboxing:
20//! * trusted applications dealing with potentially malicious data
21//!   (e.g., complex file format, network request) that could exploit security vulnerabilities;
22//! * sandbox managers, container runtimes or shells launching untrusted applications.
23//!
24//! # Examples
25//!
26//! A simple example can be found with the [`path_beneath_rules()`] helper.
27//! More complex examples can be found with the [`Ruleset` documentation](Ruleset)
28//! and the [sandboxer example](https://github.com/landlock-lsm/rust-landlock/blob/master/examples/sandboxer.rs).
29//!
30//! # Current limitations
31//!
32//! This crate exposes the Landlock features available as of Linux 5.19
33//! and then inherits some [kernel limitations](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#current-limitations)
34//! that will be addressed with future kernel releases
35//! (e.g., arbitrary mounts are always denied).
36//!
37//! # Compatibility
38//!
39//! Types defined in this crate are designed to enable the strictest Landlock configuration
40//! for the given kernel on which the program runs.
41//! In the default [best-effort](CompatLevel::BestEffort) mode,
42//! [`Ruleset`] will determine compatibility
43//! with the intersection of the currently running kernel's features
44//! and those required by the caller.
45//! This way, callers can distinguish between
46//! Landlock compatibility issues inherent to the current system
47//! (e.g., file names that don't exist)
48//! and misconfiguration that should be fixed in the program
49//! (e.g., empty or inconsistent access rights).
50//! [`RulesetError`] identifies such kind of errors.
51//!
52//! With [`set_compatibility(CompatLevel::BestEffort)`](Compatible::set_compatibility),
53//! users of the crate may mark Landlock features that are deemed required
54//! and other features that may be downgraded to use lower security on systems
55//! where they can't be enforced.
56//! It is discouraged to compare the system's provided [Landlock ABI](ABI) version directly,
57//! as it is difficult to track detailed ABI differences
58//! which are handled thanks to the [`Compatible`] trait.
59//!
60//! To make it easier to migrate to a new version of this library,
61//! we use the builder pattern
62//! and designed objects to require the minimal set of method arguments.
63//! Most `enum` are marked as `non_exhaustive` to enable backward-compatible evolutions.
64//!
65//! ## Test strategy
66//!
67//! Developers should test their sandboxed applications
68//! with a kernel that supports all requested Landlock features
69//! and check that [`RulesetCreated::restrict_self()`] returns a status matching
70//! [`Ok(RestrictionStatus { ruleset: RulesetStatus::FullyEnforced, no_new_privs: true, })`](RestrictionStatus)
71//! to make sure everything works as expected in an enforced sandbox.
72//! Alternatively, using [`set_compatibility(CompatLevel::HardRequirement)`](Compatible::set_compatibility)
73//! will immediately inform about unsupported Landlock features.
74//! These configurations should only depend on the test environment
75//! (e.g. [by checking an environment variable](https://github.com/landlock-lsm/rust-landlock/search?q=LANDLOCK_CRATE_TEST_ABI)).
76//! However, applications should only check that no error is returned (i.e. `Ok(_)`)
77//! and optionally log and inform users that the application is not fully sandboxed
78//! because of missing features from the running kernel.
79
80#[cfg(test)]
81#[macro_use]
82extern crate lazy_static;
83
84pub use access::{Access, HandledAccess};
85pub use compat::{CompatLevel, Compatible, LandlockStatus, ABI};
86pub use enumflags2::{make_bitflags, BitFlags};
87pub use errors::{
88    AccessError, AddRuleError, AddRulesError, CompatError, CreateRulesetError, Errno,
89    HandleAccessError, HandleAccessesError, PathBeneathError, PathFdError, RestrictSelfError,
90    RulesetError, ScopeError,
91};
92pub use fs::{path_beneath_rules, AccessFs, PathBeneath, PathFd};
93pub use net::{AccessNet, NetPort};
94pub use ruleset::{
95    RestrictionStatus, Rule, Ruleset, RulesetAttr, RulesetCreated, RulesetCreatedAttr,
96    RulesetStatus,
97};
98pub use scope::Scope;
99
100use access::PrivateHandledAccess;
101use compat::{CompatResult, CompatState, Compatibility, TailoredCompatLevel, TryCompat};
102use ruleset::PrivateRule;
103
104#[cfg(test)]
105use compat::{can_emulate, get_errno_from_landlock_status};
106#[cfg(test)]
107use errors::TestRulesetError;
108#[cfg(test)]
109use strum::IntoEnumIterator;
110
111mod access;
112mod compat;
113mod errors;
114mod fs;
115mod net;
116mod ruleset;
117mod scope;
118mod uapi;
119
120// Makes sure private traits cannot be implemented outside of this crate.
121mod private {
122    pub trait Sealed {}
123
124    impl Sealed for crate::AccessFs {}
125    impl Sealed for crate::AccessNet {}
126    impl Sealed for crate::Scope {}
127}
128
129#[cfg(test)]
130mod tests {
131    use crate::*;
132
133    // Emulate old kernel supports.
134    fn check_ruleset_support<F>(
135        partial: ABI,
136        full: Option<ABI>,
137        check: F,
138        error_if_abi_lt_partial: bool,
139    ) where
140        F: Fn(Ruleset) -> Result<RestrictionStatus, TestRulesetError> + Send + Copy + 'static,
141    {
142        // If there is no partial support, it means that `full == partial`.
143        assert!(partial <= full.unwrap_or(partial));
144        for abi in ABI::iter() {
145            // Ensures restrict_self() is called on a dedicated thread to avoid inconsistent tests.
146            let ret = std::thread::spawn(move || check(Ruleset::from(abi)))
147                .join()
148                .unwrap();
149
150            // Useful for failed tests and with cargo test -- --show-output
151            println!("Checking ABI {abi:?}: received {ret:#?}");
152            if can_emulate(abi, partial, full) {
153                if abi < partial && error_if_abi_lt_partial {
154                    // TODO: Check exact error type; this may require better error types.
155                    assert!(matches!(ret, Err(TestRulesetError::Ruleset(_))));
156                } else {
157                    let full_support = if let Some(full_inner) = full {
158                        abi >= full_inner
159                    } else {
160                        false
161                    };
162                    let ruleset_status = if full_support {
163                        RulesetStatus::FullyEnforced
164                    } else if abi >= partial {
165                        RulesetStatus::PartiallyEnforced
166                    } else {
167                        RulesetStatus::NotEnforced
168                    };
169                    let landlock_status = abi.into();
170                    println!("Expecting ruleset status {ruleset_status:?}");
171                    println!("Expecting Landlock status {landlock_status:?}");
172                    assert!(matches!(
173                        ret,
174                        Ok(RestrictionStatus {
175                            ruleset,
176                            landlock,
177                            no_new_privs: true,
178                        }) if ruleset == ruleset_status && landlock == landlock_status
179                    ))
180                }
181            } else {
182                // The errno value should be ENOSYS, EOPNOTSUPP, EINVAL (e.g. when an unknown
183                // access right is provided), or E2BIG (e.g. when there is an unknown field in a
184                // Landlock syscall attribute).
185                let errno = get_errno_from_landlock_status();
186                println!("Expecting error {errno:?}");
187                match ret {
188                    Err(
189                        ref error @ TestRulesetError::Ruleset(RulesetError::CreateRuleset(
190                            CreateRulesetError::CreateRulesetCall { ref source },
191                        )),
192                    ) => {
193                        assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
194                        match (source.raw_os_error(), errno) {
195                            (Some(e1), Some(e2)) => assert_eq!(e1, e2),
196                            (Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
197                            _ => unreachable!(),
198                        }
199                    }
200                    _ => unreachable!(),
201                }
202            }
203        }
204    }
205
206    #[test]
207    fn allow_root_compat() {
208        let abi = ABI::V1;
209
210        check_ruleset_support(
211            abi,
212            Some(abi),
213            move |ruleset: Ruleset| -> _ {
214                Ok(ruleset
215                    .handle_access(AccessFs::from_all(abi))?
216                    .create()?
217                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
218                    .restrict_self()?)
219            },
220            false,
221        );
222    }
223
224    #[test]
225    fn too_much_access_rights_for_a_file() {
226        let abi = ABI::V1;
227
228        check_ruleset_support(
229            abi,
230            Some(abi),
231            move |ruleset: Ruleset| -> _ {
232                Ok(ruleset
233                    .handle_access(AccessFs::from_all(abi))?
234                    .create()?
235                    // Same code as allow_root_compat() but with /etc/passwd instead of /
236                    .add_rule(PathBeneath::new(
237                        PathFd::new("/etc/passwd")?,
238                        // Only allow legitimate access rights on a file.
239                        AccessFs::from_file(abi),
240                    ))?
241                    .restrict_self()?)
242            },
243            false,
244        );
245
246        check_ruleset_support(
247            abi,
248            None,
249            move |ruleset: Ruleset| -> _ {
250                Ok(ruleset
251                    .handle_access(AccessFs::from_all(abi))?
252                    .create()?
253                    // Same code as allow_root_compat() but with /etc/passwd instead of /
254                    .add_rule(PathBeneath::new(
255                        PathFd::new("/etc/passwd")?,
256                        // Tries to allow all access rights on a file.
257                        AccessFs::from_all(abi),
258                    ))?
259                    .restrict_self()?)
260            },
261            false,
262        );
263    }
264
265    #[test]
266    fn path_beneath_rules_with_too_much_access_rights_for_a_file() {
267        let abi = ABI::V1;
268
269        check_ruleset_support(
270            abi,
271            Some(abi),
272            move |ruleset: Ruleset| -> _ {
273                Ok(ruleset
274                    .handle_access(AccessFs::from_all(ABI::V1))?
275                    .create()?
276                    // Same code as too_much_access_rights_for_a_file() but using path_beneath_rules()
277                    .add_rules(path_beneath_rules(["/etc/passwd"], AccessFs::from_all(abi)))?
278                    .restrict_self()?)
279            },
280            false,
281        );
282    }
283
284    #[test]
285    fn allow_root_fragile() {
286        let abi = ABI::V1;
287
288        check_ruleset_support(
289            abi,
290            Some(abi),
291            move |ruleset: Ruleset| -> _ {
292                // Sets default support requirement: abort the whole sandboxing for any Landlock error.
293                Ok(ruleset
294                    // Must have at least the execute check…
295                    .set_compatibility(CompatLevel::HardRequirement)
296                    .handle_access(AccessFs::Execute)?
297                    // …and possibly others.
298                    .set_compatibility(CompatLevel::BestEffort)
299                    .handle_access(AccessFs::from_all(abi))?
300                    .create()?
301                    .set_no_new_privs(true)
302                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
303                    .restrict_self()?)
304            },
305            true,
306        );
307    }
308
309    #[test]
310    fn ruleset_enforced() {
311        let abi = ABI::V1;
312
313        check_ruleset_support(
314            abi,
315            Some(abi),
316            move |ruleset: Ruleset| -> _ {
317                Ok(ruleset
318                    // Restricting without rule exceptions is legitimate to forbid a set of actions.
319                    .handle_access(AccessFs::Execute)?
320                    .create()?
321                    .restrict_self()?)
322            },
323            false,
324        );
325    }
326
327    #[test]
328    fn abi_v2_exec_refer() {
329        check_ruleset_support(
330            ABI::V1,
331            Some(ABI::V2),
332            move |ruleset: Ruleset| -> _ {
333                Ok(ruleset
334                    .handle_access(AccessFs::Execute)?
335                    // AccessFs::Refer is not supported by ABI::V1 (best-effort).
336                    .handle_access(AccessFs::Refer)?
337                    .create()?
338                    .restrict_self()?)
339            },
340            false,
341        );
342    }
343
344    #[test]
345    fn abi_v2_refer_only() {
346        // When no access is handled, do not try to create a ruleset without access.
347        check_ruleset_support(
348            ABI::V2,
349            Some(ABI::V2),
350            move |ruleset: Ruleset| -> _ {
351                Ok(ruleset
352                    .handle_access(AccessFs::Refer)?
353                    .create()?
354                    .restrict_self()?)
355            },
356            false,
357        );
358    }
359
360    #[test]
361    fn abi_v3_truncate() {
362        check_ruleset_support(
363            ABI::V2,
364            Some(ABI::V3),
365            move |ruleset: Ruleset| -> _ {
366                Ok(ruleset
367                    .handle_access(AccessFs::Refer)?
368                    .handle_access(AccessFs::Truncate)?
369                    .create()?
370                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Refer))?
371                    .restrict_self()?)
372            },
373            false,
374        );
375    }
376
377    #[test]
378    fn ruleset_created_try_clone() {
379        check_ruleset_support(
380            ABI::V1,
381            Some(ABI::V1),
382            move |ruleset: Ruleset| -> _ {
383                Ok(ruleset
384                    .handle_access(AccessFs::Execute)?
385                    .create()?
386                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Execute))?
387                    .try_clone()?
388                    .restrict_self()?)
389            },
390            false,
391        );
392    }
393
394    #[test]
395    fn abi_v4_tcp() {
396        check_ruleset_support(
397            ABI::V3,
398            Some(ABI::V4),
399            move |ruleset: Ruleset| -> _ {
400                Ok(ruleset
401                    .handle_access(AccessFs::Truncate)?
402                    .handle_access(AccessNet::BindTcp | AccessNet::ConnectTcp)?
403                    .create()?
404                    .add_rule(NetPort::new(1, AccessNet::ConnectTcp))?
405                    .restrict_self()?)
406            },
407            false,
408        );
409    }
410
411    #[test]
412    fn abi_v5_ioctl_dev() {
413        check_ruleset_support(
414            ABI::V4,
415            Some(ABI::V5),
416            move |ruleset: Ruleset| -> _ {
417                Ok(ruleset
418                    .handle_access(AccessNet::BindTcp)?
419                    .handle_access(AccessFs::IoctlDev)?
420                    .create()?
421                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::IoctlDev))?
422                    .restrict_self()?)
423            },
424            false,
425        );
426    }
427
428    #[test]
429    fn abi_v6_scope_mix() {
430        check_ruleset_support(
431            ABI::V5,
432            Some(ABI::V6),
433            move |ruleset: Ruleset| -> _ {
434                Ok(ruleset
435                    .handle_access(AccessFs::IoctlDev)?
436                    .scope(Scope::AbstractUnixSocket | Scope::Signal)?
437                    .create()?
438                    .restrict_self()?)
439            },
440            false,
441        );
442    }
443
444    #[test]
445    fn abi_v6_scope_only() {
446        check_ruleset_support(
447            ABI::V6,
448            Some(ABI::V6),
449            move |ruleset: Ruleset| -> _ {
450                Ok(ruleset
451                    .scope(Scope::AbstractUnixSocket | Scope::Signal)?
452                    .create()?
453                    .restrict_self()?)
454            },
455            false,
456        );
457    }
458
459    #[test]
460    fn ruleset_created_try_clone_ownedfd() {
461        use std::os::unix::io::{AsRawFd, OwnedFd};
462
463        let abi = ABI::V1;
464        check_ruleset_support(
465            abi,
466            Some(abi),
467            move |ruleset: Ruleset| -> _ {
468                let ruleset1 = ruleset.handle_access(AccessFs::from_all(abi))?.create()?;
469                let ruleset2 = ruleset1.try_clone().unwrap();
470                let ruleset3 = ruleset2.try_clone().unwrap();
471
472                let some1: Option<OwnedFd> = ruleset1.into();
473                if let Some(fd1) = some1 {
474                    assert!(fd1.as_raw_fd() >= 0);
475
476                    let some2: Option<OwnedFd> = ruleset2.into();
477                    let fd2 = some2.unwrap();
478                    assert!(fd2.as_raw_fd() >= 0);
479
480                    assert_ne!(fd1.as_raw_fd(), fd2.as_raw_fd());
481                }
482                Ok(ruleset3.restrict_self()?)
483            },
484            false,
485        );
486    }
487}