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.71
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 errata::Erratum;
88pub use errors::{
89    AccessError, AddRuleError, AddRulesError, CompatError, CreateRulesetError, Errno,
90    HandleAccessError, HandleAccessesError, PathBeneathError, PathFdError, RestrictSelfError,
91    RulesetError, ScopeError,
92};
93pub use fs::{path_beneath_rules, AccessFs, PathBeneath, PathFd};
94pub use net::{AccessNet, NetPort};
95pub use ruleset::{
96    RestrictionStatus, Rule, Ruleset, RulesetAttr, RulesetCreated, RulesetCreatedAttr,
97    RulesetStatus,
98};
99pub use scope::Scope;
100
101use access::PrivateHandledAccess;
102use compat::{CompatResult, CompatState, Compatibility, TailoredCompatLevel, TryCompat};
103use ruleset::PrivateRule;
104
105#[cfg(test)]
106use compat::{can_emulate, get_errno_from_landlock_status};
107#[cfg(test)]
108use errors::TestRulesetError;
109#[cfg(test)]
110use strum::IntoEnumIterator;
111
112mod access;
113mod compat;
114mod errata;
115mod errors;
116mod fs;
117mod net;
118mod ruleset;
119mod scope;
120mod uapi;
121
122// Makes sure private traits cannot be implemented outside of this crate.
123mod private {
124    pub trait Sealed {}
125
126    impl Sealed for crate::AccessFs {}
127    impl Sealed for crate::AccessNet {}
128    impl Sealed for crate::Scope {}
129}
130
131#[cfg(test)]
132mod tests {
133    use crate::*;
134
135    // Emulate old kernel supports.
136    fn check_ruleset_support<F>(
137        partial: ABI,
138        full: Option<ABI>,
139        check: F,
140        error_if_abi_lt_partial: bool,
141    ) where
142        F: Fn(Ruleset) -> Result<RestrictionStatus, TestRulesetError> + Send + Copy + 'static,
143    {
144        // If there is no partial support, it means that `full == partial`.
145        assert!(partial <= full.unwrap_or(partial));
146        for abi in ABI::iter() {
147            // Ensures restrict_self() is called on a dedicated thread to avoid inconsistent tests.
148            let ret = std::thread::spawn(move || check(Ruleset::from(abi)))
149                .join()
150                .unwrap();
151
152            // Useful for failed tests and with cargo test -- --show-output
153            println!("Checking ABI {abi:?}: received {ret:#?}");
154            if can_emulate(abi, partial, full) {
155                if abi < partial && error_if_abi_lt_partial {
156                    // TODO: Check exact error type; this may require better error types.
157                    assert!(matches!(ret, Err(TestRulesetError::Ruleset(_))));
158                } else {
159                    let full_support = if let Some(full_inner) = full {
160                        abi >= full_inner
161                    } else {
162                        false
163                    };
164                    let ruleset_status = if full_support {
165                        RulesetStatus::FullyEnforced
166                    } else if abi >= partial {
167                        RulesetStatus::PartiallyEnforced
168                    } else {
169                        RulesetStatus::NotEnforced
170                    };
171                    let landlock_status = abi.into();
172                    println!("Expecting ruleset status {ruleset_status:?}");
173                    println!("Expecting Landlock status {landlock_status:?}");
174                    assert!(matches!(
175                        ret,
176                        Ok(RestrictionStatus {
177                            ruleset,
178                            landlock,
179                            no_new_privs: true,
180                        }) if ruleset == ruleset_status && landlock == landlock_status
181                    ))
182                }
183            } else {
184                // The errno value should be ENOSYS, EOPNOTSUPP, EINVAL (e.g. when an unknown
185                // access right is provided), or E2BIG (e.g. when there is an unknown field in a
186                // Landlock syscall attribute).
187                let errno = get_errno_from_landlock_status();
188                println!("Expecting error {errno:?}");
189                match ret {
190                    Err(
191                        ref error @ TestRulesetError::Ruleset(RulesetError::CreateRuleset(
192                            CreateRulesetError::CreateRulesetCall { ref source },
193                        )),
194                    ) => {
195                        assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
196                        match (source.raw_os_error(), errno) {
197                            (Some(e1), Some(e2)) => assert_eq!(e1, e2),
198                            (Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
199                            _ => unreachable!(),
200                        }
201                    }
202                    _ => unreachable!(),
203                }
204            }
205        }
206    }
207
208    #[test]
209    fn allow_root_compat() {
210        let abi = ABI::V1;
211
212        check_ruleset_support(
213            abi,
214            Some(abi),
215            move |ruleset: Ruleset| -> _ {
216                Ok(ruleset
217                    .handle_access(AccessFs::from_all(abi))?
218                    .create()?
219                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
220                    .restrict_self()?)
221            },
222            false,
223        );
224    }
225
226    #[test]
227    fn too_much_access_rights_for_a_file() {
228        let abi = ABI::V1;
229
230        check_ruleset_support(
231            abi,
232            Some(abi),
233            move |ruleset: Ruleset| -> _ {
234                Ok(ruleset
235                    .handle_access(AccessFs::from_all(abi))?
236                    .create()?
237                    // Same code as allow_root_compat() but with /etc/passwd instead of /
238                    .add_rule(PathBeneath::new(
239                        PathFd::new("/etc/passwd")?,
240                        // Only allow legitimate access rights on a file.
241                        AccessFs::from_file(abi),
242                    ))?
243                    .restrict_self()?)
244            },
245            false,
246        );
247
248        check_ruleset_support(
249            abi,
250            None,
251            move |ruleset: Ruleset| -> _ {
252                Ok(ruleset
253                    .handle_access(AccessFs::from_all(abi))?
254                    .create()?
255                    // Same code as allow_root_compat() but with /etc/passwd instead of /
256                    .add_rule(PathBeneath::new(
257                        PathFd::new("/etc/passwd")?,
258                        // Tries to allow all access rights on a file.
259                        AccessFs::from_all(abi),
260                    ))?
261                    .restrict_self()?)
262            },
263            false,
264        );
265    }
266
267    #[test]
268    fn path_beneath_rules_with_too_much_access_rights_for_a_file() {
269        let abi = ABI::V1;
270
271        check_ruleset_support(
272            abi,
273            Some(abi),
274            move |ruleset: Ruleset| -> _ {
275                Ok(ruleset
276                    .handle_access(AccessFs::from_all(ABI::V1))?
277                    .create()?
278                    // Same code as too_much_access_rights_for_a_file() but using path_beneath_rules()
279                    .add_rules(path_beneath_rules(["/etc/passwd"], AccessFs::from_all(abi)))?
280                    .restrict_self()?)
281            },
282            false,
283        );
284    }
285
286    #[test]
287    fn allow_root_fragile() {
288        let abi = ABI::V1;
289
290        check_ruleset_support(
291            abi,
292            Some(abi),
293            move |ruleset: Ruleset| -> _ {
294                // Sets default support requirement: abort the whole sandboxing for any Landlock error.
295                Ok(ruleset
296                    // Must have at least the execute check…
297                    .set_compatibility(CompatLevel::HardRequirement)
298                    .handle_access(AccessFs::Execute)?
299                    // …and possibly others.
300                    .set_compatibility(CompatLevel::BestEffort)
301                    .handle_access(AccessFs::from_all(abi))?
302                    .create()?
303                    .set_no_new_privs(true)
304                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
305                    .restrict_self()?)
306            },
307            true,
308        );
309    }
310
311    #[test]
312    fn ruleset_enforced() {
313        let abi = ABI::V1;
314
315        check_ruleset_support(
316            abi,
317            Some(abi),
318            move |ruleset: Ruleset| -> _ {
319                Ok(ruleset
320                    // Restricting without rule exceptions is legitimate to forbid a set of actions.
321                    .handle_access(AccessFs::Execute)?
322                    .create()?
323                    .restrict_self()?)
324            },
325            false,
326        );
327    }
328
329    #[test]
330    fn abi_v2_exec_refer() {
331        check_ruleset_support(
332            ABI::V1,
333            Some(ABI::V2),
334            move |ruleset: Ruleset| -> _ {
335                Ok(ruleset
336                    .handle_access(AccessFs::Execute)?
337                    // AccessFs::Refer is not supported by ABI::V1 (best-effort).
338                    .handle_access(AccessFs::Refer)?
339                    .create()?
340                    .restrict_self()?)
341            },
342            false,
343        );
344    }
345
346    #[test]
347    fn abi_v2_refer_only() {
348        // When no access is handled, do not try to create a ruleset without access.
349        check_ruleset_support(
350            ABI::V2,
351            Some(ABI::V2),
352            move |ruleset: Ruleset| -> _ {
353                Ok(ruleset
354                    .handle_access(AccessFs::Refer)?
355                    .create()?
356                    .restrict_self()?)
357            },
358            false,
359        );
360    }
361
362    #[test]
363    fn abi_v3_truncate() {
364        check_ruleset_support(
365            ABI::V2,
366            Some(ABI::V3),
367            move |ruleset: Ruleset| -> _ {
368                Ok(ruleset
369                    .handle_access(AccessFs::Refer)?
370                    .handle_access(AccessFs::Truncate)?
371                    .create()?
372                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Refer))?
373                    .restrict_self()?)
374            },
375            false,
376        );
377    }
378
379    #[test]
380    fn ruleset_created_try_clone() {
381        check_ruleset_support(
382            ABI::V1,
383            Some(ABI::V1),
384            move |ruleset: Ruleset| -> _ {
385                Ok(ruleset
386                    .handle_access(AccessFs::Execute)?
387                    .create()?
388                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Execute))?
389                    .try_clone()?
390                    .restrict_self()?)
391            },
392            false,
393        );
394    }
395
396    #[test]
397    fn abi_v4_tcp() {
398        check_ruleset_support(
399            ABI::V3,
400            Some(ABI::V4),
401            move |ruleset: Ruleset| -> _ {
402                Ok(ruleset
403                    .handle_access(AccessFs::Truncate)?
404                    .handle_access(AccessNet::BindTcp | AccessNet::ConnectTcp)?
405                    .create()?
406                    .add_rule(NetPort::new(1, AccessNet::ConnectTcp))?
407                    .restrict_self()?)
408            },
409            false,
410        );
411    }
412
413    #[test]
414    fn abi_v5_ioctl_dev() {
415        check_ruleset_support(
416            ABI::V4,
417            Some(ABI::V5),
418            move |ruleset: Ruleset| -> _ {
419                Ok(ruleset
420                    .handle_access(AccessNet::BindTcp)?
421                    .handle_access(AccessFs::IoctlDev)?
422                    .create()?
423                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::IoctlDev))?
424                    .restrict_self()?)
425            },
426            false,
427        );
428    }
429
430    #[test]
431    fn abi_v6_scope_mix() {
432        check_ruleset_support(
433            ABI::V5,
434            Some(ABI::V6),
435            move |ruleset: Ruleset| -> _ {
436                Ok(ruleset
437                    .handle_access(AccessFs::IoctlDev)?
438                    .scope(Scope::AbstractUnixSocket | Scope::Signal)?
439                    .create()?
440                    .restrict_self()?)
441            },
442            false,
443        );
444    }
445
446    #[test]
447    fn abi_v6_scope_only() {
448        check_ruleset_support(
449            ABI::V6,
450            Some(ABI::V6),
451            move |ruleset: Ruleset| -> _ {
452                Ok(ruleset
453                    .scope(Scope::AbstractUnixSocket | Scope::Signal)?
454                    .create()?
455                    .restrict_self()?)
456            },
457            false,
458        );
459    }
460
461    #[test]
462    fn ruleset_created_try_clone_ownedfd() {
463        use std::os::unix::io::{AsRawFd, OwnedFd};
464
465        let abi = ABI::V1;
466        check_ruleset_support(
467            abi,
468            Some(abi),
469            move |ruleset: Ruleset| -> _ {
470                let ruleset1 = ruleset.handle_access(AccessFs::from_all(abi))?.create()?;
471                let ruleset2 = ruleset1.try_clone().unwrap();
472                let ruleset3 = ruleset2.try_clone().unwrap();
473
474                let some1: Option<OwnedFd> = ruleset1.into();
475                if let Some(fd1) = some1 {
476                    assert!(fd1.as_raw_fd() >= 0);
477
478                    let some2: Option<OwnedFd> = ruleset2.into();
479                    let fd2 = some2.unwrap();
480                    assert!(fd2.as_raw_fd() >= 0);
481
482                    assert_ne!(fd1.as_raw_fd(), fd2.as_raw_fd());
483                }
484                Ok(ruleset3.restrict_self()?)
485            },
486            false,
487        );
488    }
489}