landlock/
lib.rs

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