Skip to main content

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 6.15 (Landlock [ABI v7](ABI::V7))
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//! ## Audit logging
81//!
82//! Landlock ABI v7 adds control over audit logging through boolean setters, especially
83//! [`log_new_exec()`](RulesetCreatedAttr::log_new_exec)) which is useful (but noisy) for sandboxer
84//! tools.
85
86#[cfg(test)]
87#[macro_use]
88extern crate lazy_static;
89
90pub use access::{Access, HandledAccess};
91pub use compat::{CompatLevel, Compatible, LandlockStatus, ABI};
92pub use enumflags2::{make_bitflags, BitFlags};
93pub use errata::Erratum;
94pub use errors::{
95    AccessError, AddRuleError, AddRulesError, CompatError, CreateRulesetError, Errno,
96    HandleAccessError, HandleAccessesError, PathBeneathError, PathFdError, RestrictSelfError,
97    RulesetError, ScopeError, SyscallFlagError,
98};
99pub use flags::{RestrictSelfFlag, SyscallFlag};
100pub use fs::{path_beneath_rules, AccessFs, PathBeneath, PathFd};
101pub use net::{AccessNet, NetPort};
102pub use restrict_self::{RestrictSelf, RestrictSelfAttr, RestrictSelfStatus};
103pub use ruleset::{
104    RestrictionStatus, Rule, Ruleset, RulesetAttr, RulesetCreated, RulesetCreatedAttr,
105    RulesetStatus,
106};
107pub use scope::Scope;
108
109use access::PrivateHandledAccess;
110use compat::{CompatResult, CompatState, Compatibility, TailoredCompatLevel, TryCompat};
111use ruleset::PrivateRule;
112
113#[cfg(test)]
114use compat::{can_emulate, get_errno_from_landlock_status};
115#[cfg(test)]
116use errors::TestRulesetError;
117#[cfg(test)]
118use strum::IntoEnumIterator;
119
120mod access;
121mod compat;
122mod errata;
123mod errors;
124mod flags;
125mod fs;
126mod net;
127mod prctl;
128mod restrict_self;
129mod ruleset;
130mod scope;
131mod uapi;
132
133// Makes sure private traits cannot be implemented outside of this crate.
134mod private {
135    pub trait Sealed {}
136
137    impl Sealed for crate::AccessFs {}
138    impl Sealed for crate::AccessNet {}
139    impl Sealed for crate::Scope {}
140    impl Sealed for crate::RestrictSelfFlag {}
141}
142
143#[cfg(test)]
144mod tests {
145    use crate::*;
146
147    // These integration tests exercise the full builder-to-syscall path via
148    // check_ruleset_support().  Other tests in compat.rs and errata.rs make
149    // read-only kernel queries (LandlockStatus::current(), Erratum::current())
150    // but do not create rulesets or restrict threads.  All remaining tests use
151    // Ruleset::from(ABI) to exercise the builder logic and compatibility
152    // engine without kernel interaction.
153
154    // Emulate old kernel supports.  Iterates each ABI variant, mocks the
155    // builder via B::from(abi), runs the closure on a dedicated thread, and
156    // dispatches to the caller's assertion closures based on whether the
157    // mocked ABI is emulatable on the running kernel.
158    fn check_support<B, S, F, OkFn, ErrFn>(
159        partial: ABI,
160        full: Option<ABI>,
161        check: F,
162        assert_ok: OkFn,
163        assert_err: ErrFn,
164    ) where
165        B: From<ABI> + Send + 'static,
166        F: Fn(B) -> Result<S, TestRulesetError> + Send + Copy + 'static,
167        S: std::fmt::Debug + Send + 'static,
168        OkFn: Fn(ABI, Result<S, TestRulesetError>),
169        ErrFn: Fn(Result<S, TestRulesetError>),
170    {
171        // If there is no partial support, it means that `full == partial`.
172        assert!(partial <= full.unwrap_or(partial));
173        for abi in ABI::iter() {
174            // Ensures restrict_self() is called on a dedicated thread to avoid inconsistent tests.
175            let ret = std::thread::spawn(move || check(B::from(abi)))
176                .join()
177                .unwrap();
178
179            // Useful for failed tests and with cargo test -- --show-output
180            println!("Checking ABI {abi:?}: received {ret:#?}");
181            if can_emulate(abi, partial, full) {
182                assert_ok(abi, ret);
183            } else {
184                assert_err(ret);
185            }
186        }
187    }
188
189    fn check_ruleset_support<F>(
190        partial: ABI,
191        full: Option<ABI>,
192        check: F,
193        error_if_abi_lt_partial: bool,
194    ) where
195        F: Fn(Ruleset) -> Result<RestrictionStatus, TestRulesetError> + Send + Copy + 'static,
196    {
197        check_support(
198            partial,
199            full,
200            check,
201            |abi, ret| {
202                if abi < partial && error_if_abi_lt_partial {
203                    // TODO: Check exact error type; this may require better error types.
204                    assert!(matches!(ret, Err(TestRulesetError::Ruleset(_))));
205                } else {
206                    let full_support = if let Some(full_inner) = full {
207                        abi >= full_inner
208                    } else {
209                        false
210                    };
211                    let ruleset_status = if full_support {
212                        RulesetStatus::FullyEnforced
213                    } else if abi >= partial {
214                        RulesetStatus::PartiallyEnforced
215                    } else {
216                        RulesetStatus::NotEnforced
217                    };
218                    let landlock_status = abi.into();
219                    println!("Expecting ruleset status {ruleset_status:?}");
220                    println!("Expecting Landlock status {landlock_status:?}");
221                    assert!(matches!(
222                        ret,
223                        Ok(RestrictionStatus {
224                            ruleset,
225                            landlock,
226                            no_new_privs: true,
227                            ..
228                        }) if ruleset == ruleset_status && landlock == landlock_status
229                    ))
230                }
231            },
232            |ret| {
233                // The errno value should be ENOSYS, EOPNOTSUPP, EINVAL (e.g. when an unknown
234                // access right is provided), or E2BIG (e.g. when there is an unknown field in a
235                // Landlock syscall attribute).
236                let errno = get_errno_from_landlock_status();
237                println!("Expecting error {errno:?}");
238                match ret {
239                    Err(
240                        ref error @ TestRulesetError::Ruleset(RulesetError::CreateRuleset(
241                            CreateRulesetError::CreateRulesetCall { ref source },
242                        )),
243                    ) => {
244                        assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
245                        match (source.raw_os_error(), errno) {
246                            (Some(e1), Some(e2)) => assert_eq!(e1, e2),
247                            (Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
248                            _ => unreachable!(),
249                        }
250                    }
251                    // restrict_self flags may be rejected by the kernel with EINVAL
252                    // when the mock ABI is higher than the running kernel's ABI.
253                    Err(TestRulesetError::Ruleset(RulesetError::RestrictSelf(
254                        RestrictSelfError::RestrictSelfCall { ref source },
255                    ))) => {
256                        assert_eq!(source.raw_os_error(), Some(libc::EINVAL));
257                    }
258                    _ => unreachable!(),
259                }
260            },
261        );
262    }
263
264    // Emulate old kernel supports for the domain-less RestrictSelf builder.
265    //
266    // Unlike check_ruleset_support, RestrictSelf does not create a Landlock domain, so there is no
267    // ruleset enforcement status to assert.  We verify the kernel probe (status.landlock) and
268    // propagate any syscall error.  RestrictSelf::apply() enforces PR_SET_NO_NEW_PRIVS by default.
269    fn check_restrict_self_support<F>(partial: ABI, full: Option<ABI>, check: F)
270    where
271        F: Fn(RestrictSelf) -> Result<RestrictSelfStatus, TestRulesetError> + Send + Copy + 'static,
272    {
273        check_support(
274            partial,
275            full,
276            check,
277            |abi, ret| {
278                let landlock_status: LandlockStatus = abi.into();
279                println!("Expecting Landlock status {landlock_status:?}");
280                assert!(matches!(
281                    ret,
282                    Ok(RestrictSelfStatus { landlock, .. }) if landlock == landlock_status
283                ));
284            },
285            |ret| {
286                // The errno value should be ENOSYS, EOPNOTSUPP, or EINVAL (e.g. when actual_flags
287                // carries bits unknown to the running kernel).  Unlike check_ruleset_support,
288                // landlock_restrict_self() is the first syscall here, so ENOSYS is possible when
289                // Landlock is unavailable.
290                let errno = get_errno_from_landlock_status();
291                println!("Expecting error {errno:?}");
292                match ret {
293                    Err(
294                        ref error @ TestRulesetError::Ruleset(RulesetError::RestrictSelf(
295                            RestrictSelfError::RestrictSelfCall { ref source },
296                        )),
297                    ) => {
298                        assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
299                        match (source.raw_os_error(), errno) {
300                            (Some(e1), Some(e2)) => assert_eq!(e1, e2),
301                            (Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
302                            _ => unreachable!(),
303                        }
304                    }
305                    _ => unreachable!(),
306                }
307            },
308        );
309    }
310
311    #[test]
312    fn allow_root_compat() {
313        let abi = ABI::V1;
314
315        check_ruleset_support(
316            abi,
317            Some(abi),
318            move |ruleset: Ruleset| -> _ {
319                Ok(ruleset
320                    .handle_access(AccessFs::from_all(abi))?
321                    .create()?
322                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
323                    .restrict_self()?)
324            },
325            false,
326        );
327    }
328
329    #[test]
330    fn too_much_access_rights_for_a_file() {
331        let abi = ABI::V1;
332
333        check_ruleset_support(
334            abi,
335            Some(abi),
336            move |ruleset: Ruleset| -> _ {
337                Ok(ruleset
338                    .handle_access(AccessFs::from_all(abi))?
339                    .create()?
340                    // Same code as allow_root_compat() but with /etc/passwd instead of /
341                    .add_rule(PathBeneath::new(
342                        PathFd::new("/etc/passwd")?,
343                        // Only allow legitimate access rights on a file.
344                        AccessFs::from_file(abi),
345                    ))?
346                    .restrict_self()?)
347            },
348            false,
349        );
350
351        check_ruleset_support(
352            abi,
353            None,
354            move |ruleset: Ruleset| -> _ {
355                Ok(ruleset
356                    .handle_access(AccessFs::from_all(abi))?
357                    .create()?
358                    // Same code as allow_root_compat() but with /etc/passwd instead of /
359                    .add_rule(PathBeneath::new(
360                        PathFd::new("/etc/passwd")?,
361                        // Tries to allow all access rights on a file.
362                        AccessFs::from_all(abi),
363                    ))?
364                    .restrict_self()?)
365            },
366            false,
367        );
368    }
369
370    #[test]
371    fn path_beneath_rules_with_too_much_access_rights_for_a_file() {
372        let abi = ABI::V1;
373
374        check_ruleset_support(
375            abi,
376            Some(abi),
377            move |ruleset: Ruleset| -> _ {
378                Ok(ruleset
379                    .handle_access(AccessFs::from_all(ABI::V1))?
380                    .create()?
381                    // Same code as too_much_access_rights_for_a_file() but using path_beneath_rules()
382                    .add_rules(path_beneath_rules(["/etc/passwd"], AccessFs::from_all(abi)))?
383                    .restrict_self()?)
384            },
385            false,
386        );
387    }
388
389    #[test]
390    fn allow_root_fragile() {
391        let abi = ABI::V1;
392
393        check_ruleset_support(
394            abi,
395            Some(abi),
396            move |ruleset: Ruleset| -> _ {
397                // Sets default support requirement: abort the whole sandboxing for any Landlock error.
398                Ok(ruleset
399                    // Must have at least the execute check…
400                    .set_compatibility(CompatLevel::HardRequirement)
401                    .handle_access(AccessFs::Execute)?
402                    // …and possibly others.
403                    .set_compatibility(CompatLevel::BestEffort)
404                    .handle_access(AccessFs::from_all(abi))?
405                    .create()?
406                    .no_new_privs(true)
407                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
408                    .restrict_self()?)
409            },
410            true,
411        );
412    }
413
414    #[test]
415    fn ruleset_enforced() {
416        let abi = ABI::V1;
417
418        check_ruleset_support(
419            abi,
420            Some(abi),
421            move |ruleset: Ruleset| -> _ {
422                Ok(ruleset
423                    // Restricting without rule exceptions is legitimate to forbid a set of actions.
424                    .handle_access(AccessFs::Execute)?
425                    .create()?
426                    .restrict_self()?)
427            },
428            false,
429        );
430    }
431
432    #[test]
433    fn abi_v2_exec_refer() {
434        check_ruleset_support(
435            ABI::V1,
436            Some(ABI::V2),
437            move |ruleset: Ruleset| -> _ {
438                Ok(ruleset
439                    .handle_access(AccessFs::Execute)?
440                    // AccessFs::Refer is not supported by ABI::V1 (best-effort).
441                    .handle_access(AccessFs::Refer)?
442                    .create()?
443                    .restrict_self()?)
444            },
445            false,
446        );
447    }
448
449    #[test]
450    fn abi_v2_refer_only() {
451        // When no access is handled, do not try to create a ruleset without access.
452        check_ruleset_support(
453            ABI::V2,
454            Some(ABI::V2),
455            move |ruleset: Ruleset| -> _ {
456                Ok(ruleset
457                    .handle_access(AccessFs::Refer)?
458                    .create()?
459                    .restrict_self()?)
460            },
461            false,
462        );
463    }
464
465    #[test]
466    fn abi_v3_truncate() {
467        check_ruleset_support(
468            ABI::V2,
469            Some(ABI::V3),
470            move |ruleset: Ruleset| -> _ {
471                Ok(ruleset
472                    .handle_access(AccessFs::Refer)?
473                    .handle_access(AccessFs::Truncate)?
474                    .create()?
475                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Refer))?
476                    .restrict_self()?)
477            },
478            false,
479        );
480    }
481
482    #[test]
483    fn ruleset_created_try_clone() {
484        check_ruleset_support(
485            ABI::V1,
486            Some(ABI::V1),
487            move |ruleset: Ruleset| -> _ {
488                Ok(ruleset
489                    .handle_access(AccessFs::Execute)?
490                    .create()?
491                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Execute))?
492                    .try_clone()?
493                    .restrict_self()?)
494            },
495            false,
496        );
497    }
498
499    #[test]
500    fn abi_v4_tcp() {
501        check_ruleset_support(
502            ABI::V3,
503            Some(ABI::V4),
504            move |ruleset: Ruleset| -> _ {
505                Ok(ruleset
506                    .handle_access(AccessFs::Truncate)?
507                    .handle_access(AccessNet::BindTcp | AccessNet::ConnectTcp)?
508                    .create()?
509                    .add_rule(NetPort::new(1, AccessNet::ConnectTcp))?
510                    .restrict_self()?)
511            },
512            false,
513        );
514    }
515
516    #[test]
517    fn abi_v5_ioctl_dev() {
518        check_ruleset_support(
519            ABI::V4,
520            Some(ABI::V5),
521            move |ruleset: Ruleset| -> _ {
522                Ok(ruleset
523                    .handle_access(AccessNet::BindTcp)?
524                    .handle_access(AccessFs::IoctlDev)?
525                    .create()?
526                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::IoctlDev))?
527                    .restrict_self()?)
528            },
529            false,
530        );
531    }
532
533    #[test]
534    fn abi_v6_scope_mix() {
535        check_ruleset_support(
536            ABI::V5,
537            Some(ABI::V6),
538            move |ruleset: Ruleset| -> _ {
539                Ok(ruleset
540                    .handle_access(AccessFs::IoctlDev)?
541                    .scope(Scope::AbstractUnixSocket | Scope::Signal)?
542                    .create()?
543                    .restrict_self()?)
544            },
545            false,
546        );
547    }
548
549    #[test]
550    fn abi_v6_scope_only() {
551        check_ruleset_support(
552            ABI::V6,
553            Some(ABI::V6),
554            move |ruleset: Ruleset| -> _ {
555                Ok(ruleset
556                    .scope(Scope::AbstractUnixSocket | Scope::Signal)?
557                    .create()?
558                    .restrict_self()?)
559            },
560            false,
561        );
562    }
563
564    #[test]
565    fn abi_v7_log_flags() {
566        // Uses Scope::Signal to get partial enforcement at V6 (scopes supported
567        // but log flags not).
568        check_ruleset_support(
569            ABI::V6,
570            Some(ABI::V7),
571            move |ruleset: Ruleset| -> _ {
572                let status = ruleset
573                    .scope(Scope::Signal)?
574                    .create()?
575                    .log_same_exec(false)?
576                    .log_new_exec(true)?
577                    .log_subdomains(false)?
578                    .restrict_self()?;
579
580                if status.ruleset == RulesetStatus::FullyEnforced {
581                    assert!(!status.log_same_exec);
582                    assert!(status.log_new_exec);
583                    assert!(!status.log_subdomains);
584                } else {
585                    assert!(status.log_same_exec);
586                    assert!(!status.log_new_exec);
587                    assert!(status.log_subdomains);
588                }
589
590                Ok(status)
591            },
592            false,
593        );
594    }
595
596    #[test]
597    fn ruleset_created_try_clone_ownedfd() {
598        use std::os::unix::io::{AsRawFd, OwnedFd};
599
600        let abi = ABI::V1;
601        check_ruleset_support(
602            abi,
603            Some(abi),
604            move |ruleset: Ruleset| -> _ {
605                let ruleset1 = ruleset.handle_access(AccessFs::from_all(abi))?.create()?;
606                let ruleset2 = ruleset1.try_clone().unwrap();
607                let ruleset3 = ruleset2.try_clone().unwrap();
608
609                let some1: Option<OwnedFd> = ruleset1.into();
610                if let Some(fd1) = some1 {
611                    assert!(fd1.as_raw_fd() >= 0);
612
613                    let some2: Option<OwnedFd> = ruleset2.into();
614                    let fd2 = some2.unwrap();
615                    assert!(fd2.as_raw_fd() >= 0);
616
617                    assert_ne!(fd1.as_raw_fd(), fd2.as_raw_fd());
618                }
619                Ok(ruleset3.restrict_self()?)
620            },
621            false,
622        );
623    }
624
625    #[test]
626    fn restrict_self_log_subdomains() {
627        check_restrict_self_support(ABI::V7, Some(ABI::V7), move |rs: RestrictSelf| -> _ {
628            Ok(rs.log_subdomains(false)?.apply()?)
629        });
630    }
631}