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.77
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 7.2
33//! (Landlock [ABI v10](ABI::V10)), except for quiet access masks and quiet rules,
34//! which are not exposed by this crate yet.
35//! It then inherits some [kernel limitations](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#current-limitations)
36//! that will be addressed with future kernel releases
37//! (e.g., arbitrary mounts are always denied).
38//!
39//! # Compatibility
40//!
41//! Types defined in this crate are designed to enable the strictest Landlock configuration
42//! for the given kernel on which the program runs.
43//! In the default [best-effort](CompatLevel::BestEffort) mode,
44//! [`Ruleset`] will determine compatibility
45//! with the intersection of the currently running kernel's features
46//! and those required by the caller.
47//! This way, callers can distinguish between
48//! Landlock compatibility issues inherent to the current system
49//! (e.g., file names that don't exist)
50//! and misconfiguration that should be fixed in the program
51//! (e.g., empty or inconsistent access rights).
52//! [`RulesetError`] identifies such kind of errors.
53//!
54//! With [`set_compatibility(CompatLevel::BestEffort)`](Compatible::set_compatibility),
55//! users of the crate may mark Landlock features that are deemed required
56//! and other features that may be downgraded to use lower security on systems
57//! where they can't be enforced.
58//! It is discouraged to compare the system's provided [Landlock ABI](ABI) version directly,
59//! as it is difficult to track detailed ABI differences
60//! which are handled thanks to the [`Compatible`] trait.
61//!
62//! To make it easier to migrate to a new version of this library,
63//! we use the builder pattern
64//! and designed objects to require the minimal set of method arguments.
65//! Most `enum` are marked as `non_exhaustive` to enable backward-compatible evolutions.
66//!
67//! ## Test strategy
68//!
69//! Developers should test their sandboxed applications
70//! with a kernel that supports all requested Landlock features
71//! and check that [`RulesetCreated::restrict_self()`] returns a status matching
72//! [`Ok(RestrictionStatus { ruleset: RulesetStatus::FullyEnforced, no_new_privs: true, })`](RestrictionStatus)
73//! to make sure everything works as expected in an enforced sandbox.
74//! Alternatively, using [`set_compatibility(CompatLevel::HardRequirement)`](Compatible::set_compatibility)
75//! will immediately inform about unsupported Landlock features.
76//! These configurations should only depend on the test environment
77//! (e.g. [by checking an environment variable](https://github.com/landlock-lsm/rust-landlock/search?q=LANDLOCK_CRATE_TEST_ABI)).
78//! However, applications should only check that no error is returned (i.e. `Ok(_)`)
79//! and optionally log and inform users that the application is not fully sandboxed
80//! because of missing features from the running kernel.
81//!
82//! ## Audit logging
83//!
84//! Landlock ABI v7 adds control over audit logging through boolean setters, especially
85//! [`log_new_exec()`](RulesetCreatedAttr::log_new_exec)) which is useful (but noisy) for sandboxer
86//! tools.
87//!
88//! ## Multithreaded processes
89//!
90//! By default `landlock_restrict_self()` only restricts the calling thread.
91//! Landlock ABI v8 adds [`all_threads()`](RestrictSelfAttr::all_threads) to
92//! atomically enforce the configuration on every thread of the process.  On
93//! an older kernel this is silently dropped in the default best-effort mode,
94//! leaving sibling and parent threads unrestricted, so multithreaded programs
95//! that need this guarantee should require it (see
96//! [`all_threads()`](RestrictSelfAttr::all_threads)).
97
98#[cfg(test)]
99#[macro_use]
100extern crate lazy_static;
101
102pub use access::{Access, HandledAccess};
103pub use compat::{CompatLevel, Compatible, LandlockStatus, ABI};
104pub use enumflags2::{make_bitflags, BitFlags};
105pub use errata::Erratum;
106pub use errors::{
107    AccessError, AddRuleError, AddRulesError, CompatError, CreateRulesetError, Errno,
108    HandleAccessError, HandleAccessesError, PathBeneathError, PathFdError, RestrictSelfError,
109    RulesetError, ScopeError, SyscallFlagError,
110};
111pub use flags::{RestrictSelfFlag, SyscallFlag};
112pub use fs::{path_beneath_rules, AccessFs, PathBeneath, PathFd};
113pub use net::{AccessNet, NetPort};
114pub use restrict_self::{RestrictSelf, RestrictSelfAttr, RestrictSelfStatus};
115pub use ruleset::{
116    RestrictionStatus, Rule, Ruleset, RulesetAttr, RulesetCreated, RulesetCreatedAttr,
117    RulesetStatus,
118};
119pub use scope::Scope;
120
121use access::PrivateHandledAccess;
122use compat::{CompatResult, CompatState, Compatibility, TailoredCompatLevel, TryCompat};
123use ruleset::PrivateRule;
124
125#[cfg(test)]
126use compat::{can_emulate, get_errno_from_landlock_status};
127#[cfg(test)]
128use errors::TestRulesetError;
129#[cfg(test)]
130use strum::IntoEnumIterator;
131
132mod access;
133mod compat;
134mod errata;
135mod errors;
136mod flags;
137mod fs;
138mod net;
139mod prctl;
140mod restrict_self;
141mod ruleset;
142mod scope;
143mod uapi;
144
145// Makes sure private traits cannot be implemented outside of this crate.
146mod private {
147    pub trait Sealed {}
148
149    impl Sealed for crate::AccessFs {}
150    impl Sealed for crate::AccessNet {}
151    impl Sealed for crate::Scope {}
152    impl Sealed for crate::RestrictSelfFlag {}
153}
154
155#[cfg(test)]
156mod tests {
157    use crate::*;
158
159    // These integration tests exercise the full builder-to-syscall path via
160    // check_ruleset_support().  Other tests in compat.rs and errata.rs make
161    // read-only kernel queries (LandlockStatus::current(), Erratum::current())
162    // but do not create rulesets or restrict threads.  All remaining tests use
163    // Ruleset::from(ABI) to exercise the builder logic and compatibility
164    // engine without kernel interaction.
165
166    // Emulate old kernel supports.  Iterates each ABI variant, mocks the
167    // builder via B::from(abi), runs the closure on a dedicated thread, and
168    // dispatches to the caller's assertion closures based on whether the
169    // mocked ABI is emulatable on the running kernel.
170    fn check_support<B, S, F, OkFn, ErrFn>(
171        partial: ABI,
172        full: Option<ABI>,
173        check: F,
174        assert_ok: OkFn,
175        assert_err: ErrFn,
176    ) where
177        B: From<ABI> + Send + 'static,
178        F: Fn(B) -> Result<S, TestRulesetError> + Send + Copy + 'static,
179        S: std::fmt::Debug + Send + 'static,
180        OkFn: Fn(ABI, Result<S, TestRulesetError>),
181        ErrFn: Fn(Result<S, TestRulesetError>),
182    {
183        // If there is no partial support, it means that `full == partial`.
184        assert!(partial <= full.unwrap_or(partial));
185        for abi in ABI::iter() {
186            // Ensures restrict_self() is called on a dedicated thread to avoid inconsistent tests.
187            let ret = std::thread::spawn(move || check(B::from(abi)))
188                .join()
189                .unwrap();
190
191            // Useful for failed tests and with cargo test -- --show-output
192            println!("Checking ABI {abi:?}: received {ret:#?}");
193            if can_emulate(abi, partial, full) {
194                assert_ok(abi, ret);
195            } else {
196                assert_err(ret);
197            }
198        }
199    }
200
201    fn check_ruleset_support<F>(
202        partial: ABI,
203        full: Option<ABI>,
204        check: F,
205        error_if_abi_lt_partial: bool,
206    ) where
207        F: Fn(Ruleset) -> Result<RestrictionStatus, TestRulesetError> + Send + Copy + 'static,
208    {
209        check_support(
210            partial,
211            full,
212            check,
213            |abi, ret| {
214                if abi < partial && error_if_abi_lt_partial {
215                    // TODO: Check exact error type; this may require better error types.
216                    assert!(matches!(ret, Err(TestRulesetError::Ruleset(_))));
217                } else {
218                    let full_support = if let Some(full_inner) = full {
219                        abi >= full_inner
220                    } else {
221                        false
222                    };
223                    let ruleset_status = if full_support {
224                        RulesetStatus::FullyEnforced
225                    } else if abi >= partial {
226                        RulesetStatus::PartiallyEnforced
227                    } else {
228                        RulesetStatus::NotEnforced
229                    };
230                    let landlock_status = abi.into();
231                    println!("Expecting ruleset status {ruleset_status:?}");
232                    println!("Expecting Landlock status {landlock_status:?}");
233                    assert!(matches!(
234                        ret,
235                        Ok(RestrictionStatus {
236                            ruleset,
237                            landlock,
238                            no_new_privs: true,
239                            ..
240                        }) if ruleset == ruleset_status && landlock == landlock_status
241                    ))
242                }
243            },
244            |ret| {
245                // The errno value should be ENOSYS, EOPNOTSUPP, EINVAL (e.g. when an unknown
246                // access right is provided), or E2BIG (e.g. when there is an unknown field in a
247                // Landlock syscall attribute).
248                let errno = get_errno_from_landlock_status();
249                println!("Expecting error {errno:?}");
250                match ret {
251                    Err(
252                        ref error @ TestRulesetError::Ruleset(RulesetError::CreateRuleset(
253                            CreateRulesetError::CreateRulesetCall { ref source },
254                        )),
255                    ) => {
256                        assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
257                        match (source.raw_os_error(), errno) {
258                            (Some(e1), Some(e2)) => assert_eq!(e1, e2),
259                            (Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
260                            _ => unreachable!(),
261                        }
262                    }
263                    // restrict_self flags may be rejected by the kernel with EINVAL
264                    // when the mock ABI is higher than the running kernel's ABI.
265                    Err(TestRulesetError::Ruleset(RulesetError::RestrictSelf(
266                        RestrictSelfError::RestrictSelfCall { ref source },
267                    ))) => {
268                        assert_eq!(source.raw_os_error(), Some(libc::EINVAL));
269                    }
270                    _ => unreachable!(),
271                }
272            },
273        );
274    }
275
276    // Emulate old kernel supports for the domain-less RestrictSelf builder.
277    //
278    // Unlike check_ruleset_support, RestrictSelf does not create a Landlock domain, so there is no
279    // ruleset enforcement status to assert.  We verify the kernel probe (status.landlock) and
280    // propagate any syscall error.  RestrictSelf::apply() enforces PR_SET_NO_NEW_PRIVS by default.
281    fn check_restrict_self_support<F>(partial: ABI, full: Option<ABI>, check: F)
282    where
283        F: Fn(RestrictSelf) -> Result<RestrictSelfStatus, TestRulesetError> + Send + Copy + 'static,
284    {
285        check_support(
286            partial,
287            full,
288            check,
289            |abi, ret| {
290                let landlock_status: LandlockStatus = abi.into();
291                println!("Expecting Landlock status {landlock_status:?}");
292                assert!(matches!(
293                    ret,
294                    Ok(RestrictSelfStatus { landlock, .. }) if landlock == landlock_status
295                ));
296            },
297            |ret| {
298                // The errno value should be ENOSYS, EOPNOTSUPP, or EINVAL (e.g. when actual_flags
299                // carries bits unknown to the running kernel).  Unlike check_ruleset_support,
300                // landlock_restrict_self() is the first syscall here, so ENOSYS is possible when
301                // Landlock is unavailable.
302                let errno = get_errno_from_landlock_status();
303                println!("Expecting error {errno:?}");
304                match ret {
305                    Err(
306                        ref error @ TestRulesetError::Ruleset(RulesetError::RestrictSelf(
307                            RestrictSelfError::RestrictSelfCall { ref source },
308                        )),
309                    ) => {
310                        assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
311                        match (source.raw_os_error(), errno) {
312                            (Some(e1), Some(e2)) => assert_eq!(e1, e2),
313                            (Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
314                            _ => unreachable!(),
315                        }
316                    }
317                    _ => unreachable!(),
318                }
319            },
320        );
321    }
322
323    #[test]
324    fn allow_root_compat() {
325        let abi = ABI::V1;
326
327        check_ruleset_support(
328            abi,
329            Some(abi),
330            move |ruleset: Ruleset| -> _ {
331                Ok(ruleset
332                    .handle_access(AccessFs::from_all(abi))?
333                    .create()?
334                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
335                    .restrict_self()?)
336            },
337            false,
338        );
339    }
340
341    #[test]
342    fn too_much_access_rights_for_a_file() {
343        let abi = ABI::V1;
344
345        check_ruleset_support(
346            abi,
347            Some(abi),
348            move |ruleset: Ruleset| -> _ {
349                Ok(ruleset
350                    .handle_access(AccessFs::from_all(abi))?
351                    .create()?
352                    // Same code as allow_root_compat() but with /etc/passwd instead of /
353                    .add_rule(PathBeneath::new(
354                        PathFd::new("/etc/passwd")?,
355                        // Only allow legitimate access rights on a file.
356                        AccessFs::from_file(abi),
357                    ))?
358                    .restrict_self()?)
359            },
360            false,
361        );
362
363        check_ruleset_support(
364            abi,
365            None,
366            move |ruleset: Ruleset| -> _ {
367                Ok(ruleset
368                    .handle_access(AccessFs::from_all(abi))?
369                    .create()?
370                    // Same code as allow_root_compat() but with /etc/passwd instead of /
371                    .add_rule(PathBeneath::new(
372                        PathFd::new("/etc/passwd")?,
373                        // Tries to allow all access rights on a file.
374                        AccessFs::from_all(abi),
375                    ))?
376                    .restrict_self()?)
377            },
378            false,
379        );
380    }
381
382    #[test]
383    fn path_beneath_rules_with_too_much_access_rights_for_a_file() {
384        let abi = ABI::V1;
385
386        check_ruleset_support(
387            abi,
388            Some(abi),
389            move |ruleset: Ruleset| -> _ {
390                Ok(ruleset
391                    .handle_access(AccessFs::from_all(ABI::V1))?
392                    .create()?
393                    // Same code as too_much_access_rights_for_a_file() but using path_beneath_rules()
394                    .add_rules(path_beneath_rules(["/etc/passwd"], AccessFs::from_all(abi)))?
395                    .restrict_self()?)
396            },
397            false,
398        );
399    }
400
401    #[test]
402    fn allow_root_fragile() {
403        let abi = ABI::V1;
404
405        check_ruleset_support(
406            abi,
407            Some(abi),
408            move |ruleset: Ruleset| -> _ {
409                // Sets default support requirement: abort the whole sandboxing for any Landlock error.
410                Ok(ruleset
411                    // Must have at least the execute check…
412                    .set_compatibility(CompatLevel::HardRequirement)
413                    .handle_access(AccessFs::Execute)?
414                    // …and possibly others.
415                    .set_compatibility(CompatLevel::BestEffort)
416                    .handle_access(AccessFs::from_all(abi))?
417                    .create()?
418                    .no_new_privs(true)
419                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
420                    .restrict_self()?)
421            },
422            true,
423        );
424    }
425
426    #[test]
427    fn ruleset_enforced() {
428        let abi = ABI::V1;
429
430        check_ruleset_support(
431            abi,
432            Some(abi),
433            move |ruleset: Ruleset| -> _ {
434                Ok(ruleset
435                    // Restricting without rule exceptions is legitimate to forbid a set of actions.
436                    .handle_access(AccessFs::Execute)?
437                    .create()?
438                    .restrict_self()?)
439            },
440            false,
441        );
442    }
443
444    #[test]
445    fn abi_v2_exec_refer() {
446        check_ruleset_support(
447            ABI::V1,
448            Some(ABI::V2),
449            move |ruleset: Ruleset| -> _ {
450                Ok(ruleset
451                    .handle_access(AccessFs::Execute)?
452                    // AccessFs::Refer is not supported by ABI::V1 (best-effort).
453                    .handle_access(AccessFs::Refer)?
454                    .create()?
455                    .restrict_self()?)
456            },
457            false,
458        );
459    }
460
461    #[test]
462    fn abi_v2_refer_only() {
463        // When no access is handled, do not try to create a ruleset without access.
464        check_ruleset_support(
465            ABI::V2,
466            Some(ABI::V2),
467            move |ruleset: Ruleset| -> _ {
468                Ok(ruleset
469                    .handle_access(AccessFs::Refer)?
470                    .create()?
471                    .restrict_self()?)
472            },
473            false,
474        );
475    }
476
477    #[test]
478    fn abi_v3_truncate() {
479        check_ruleset_support(
480            ABI::V2,
481            Some(ABI::V3),
482            move |ruleset: Ruleset| -> _ {
483                Ok(ruleset
484                    .handle_access(AccessFs::Refer)?
485                    .handle_access(AccessFs::Truncate)?
486                    .create()?
487                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Refer))?
488                    .restrict_self()?)
489            },
490            false,
491        );
492    }
493
494    #[test]
495    fn ruleset_created_try_clone() {
496        check_ruleset_support(
497            ABI::V1,
498            Some(ABI::V1),
499            move |ruleset: Ruleset| -> _ {
500                Ok(ruleset
501                    .handle_access(AccessFs::Execute)?
502                    .create()?
503                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Execute))?
504                    .try_clone()?
505                    .restrict_self()?)
506            },
507            false,
508        );
509    }
510
511    #[test]
512    fn abi_v4_tcp() {
513        check_ruleset_support(
514            ABI::V3,
515            Some(ABI::V4),
516            move |ruleset: Ruleset| -> _ {
517                Ok(ruleset
518                    .handle_access(AccessFs::Truncate)?
519                    .handle_access(AccessNet::BindTcp | AccessNet::ConnectTcp)?
520                    .create()?
521                    .add_rule(NetPort::new(1, AccessNet::ConnectTcp))?
522                    .restrict_self()?)
523            },
524            false,
525        );
526    }
527
528    #[test]
529    fn abi_v5_ioctl_dev() {
530        check_ruleset_support(
531            ABI::V4,
532            Some(ABI::V5),
533            move |ruleset: Ruleset| -> _ {
534                Ok(ruleset
535                    .handle_access(AccessNet::BindTcp)?
536                    .handle_access(AccessFs::IoctlDev)?
537                    .create()?
538                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::IoctlDev))?
539                    .restrict_self()?)
540            },
541            false,
542        );
543    }
544
545    #[test]
546    fn abi_v6_scope_mix() {
547        check_ruleset_support(
548            ABI::V5,
549            Some(ABI::V6),
550            move |ruleset: Ruleset| -> _ {
551                Ok(ruleset
552                    .handle_access(AccessFs::IoctlDev)?
553                    .scope(Scope::AbstractUnixSocket | Scope::Signal)?
554                    .create()?
555                    .restrict_self()?)
556            },
557            false,
558        );
559    }
560
561    #[test]
562    fn abi_v6_scope_only() {
563        check_ruleset_support(
564            ABI::V6,
565            Some(ABI::V6),
566            move |ruleset: Ruleset| -> _ {
567                Ok(ruleset
568                    .scope(Scope::AbstractUnixSocket | Scope::Signal)?
569                    .create()?
570                    .restrict_self()?)
571            },
572            false,
573        );
574    }
575
576    #[test]
577    fn abi_v7_log_flags() {
578        // Uses Scope::Signal to get partial enforcement at V6 (scopes supported
579        // but log flags not).
580        check_ruleset_support(
581            ABI::V6,
582            Some(ABI::V7),
583            move |ruleset: Ruleset| -> _ {
584                let status = ruleset
585                    .scope(Scope::Signal)?
586                    .create()?
587                    .log_same_exec(false)?
588                    .log_new_exec(true)?
589                    .log_subdomains(false)?
590                    .restrict_self()?;
591
592                if status.ruleset == RulesetStatus::FullyEnforced {
593                    assert!(!status.log_same_exec);
594                    assert!(status.log_new_exec);
595                    assert!(!status.log_subdomains);
596                } else {
597                    assert!(status.log_same_exec);
598                    assert!(!status.log_new_exec);
599                    assert!(status.log_subdomains);
600                }
601
602                Ok(status)
603            },
604            false,
605        );
606    }
607
608    #[test]
609    fn ruleset_created_try_clone_ownedfd() {
610        use std::os::unix::io::{AsRawFd, OwnedFd};
611
612        let abi = ABI::V1;
613        check_ruleset_support(
614            abi,
615            Some(abi),
616            move |ruleset: Ruleset| -> _ {
617                let ruleset1 = ruleset.handle_access(AccessFs::from_all(abi))?.create()?;
618                let ruleset2 = ruleset1.try_clone().unwrap();
619                let ruleset3 = ruleset2.try_clone().unwrap();
620
621                let some1: Option<OwnedFd> = ruleset1.into();
622                if let Some(fd1) = some1 {
623                    assert!(fd1.as_raw_fd() >= 0);
624
625                    let some2: Option<OwnedFd> = ruleset2.into();
626                    let fd2 = some2.unwrap();
627                    assert!(fd2.as_raw_fd() >= 0);
628
629                    assert_ne!(fd1.as_raw_fd(), fd2.as_raw_fd());
630                }
631                Ok(ruleset3.restrict_self()?)
632            },
633            false,
634        );
635    }
636
637    #[test]
638    fn restrict_self_log_subdomains() {
639        check_restrict_self_support(ABI::V7, Some(ABI::V7), move |rs: RestrictSelf| -> _ {
640            Ok(rs.log_subdomains(false)?.apply()?)
641        });
642    }
643
644    // ABI v8's LANDLOCK_RESTRICT_SELF_TSYNC (exposed as all_threads()) applies
645    // the domain to every thread of the process, not just the calling one.
646    // Enforcing that in the cargo-test runner would restrict every test thread,
647    // so each all_threads() setting runs in its own forked child (restrict_self
648    // is irreversible and, with TSYNC, process-wide) that hosts exactly two
649    // threads: its main thread and one sibling.  Both threads list "/" (which
650    // needs AccessFs::ReadDir, handled by from_all(ABI::V1)) before and after
651    // enforcement.
652    //
653    // Expected outcomes are pinned from TEST_ABI, the running kernel's Landlock
654    // ABI: set per kernel by CI, auto-detected locally, and asserted to match
655    // the kernel by current_kernel_abi().  It is an oracle independent of the
656    // restrict_self() status under test: the calling thread must be denied iff
657    // Landlock is enforced at all, and the sibling must be denied iff TSYNC is
658    // both requested and supported (ABI v8+).  Both the kernel's real behavior
659    // and the crate's reported status are checked against this truth, so a
660    // silently dropped or otherwise ineffective TSYNC is caught on every kernel
661    // in the CI matrix, from unsupported up to v8.
662    #[test]
663    fn abi_v8_all_threads() {
664        use crate::compat::TEST_ABI;
665        use std::sync::{Arc, Barrier};
666        use std::thread;
667
668        // Child exit code: OK, the OR of the mismatched checks, or PANICKED.
669        const OK: i32 = 0;
670        const MAIN_BEFORE_DENIED: i32 = 1 << 0;
671        const SIBLING_BEFORE_DENIED: i32 = 1 << 1;
672        const MAIN_AFTER_MISMATCH: i32 = 1 << 2;
673        const SIBLING_AFTER_MISMATCH: i32 = 1 << 3;
674        const STATUS_RULESET_MISMATCH: i32 = 1 << 4;
675        const STATUS_TSYNC_MISMATCH: i32 = 1 << 5;
676        const PANICKED: i32 = 1 << 6;
677
678        // Listing "/" needs AccessFs::ReadDir: allowed before enforcement,
679        // denied afterwards for every thread the domain covers.
680        fn can_list_root() -> bool {
681            std::fs::read_dir("/").is_ok()
682        }
683
684        // Ground truth for the running kernel, independent of the status object.
685        let landlock_supported = *TEST_ABI != ABI::Unsupported;
686        let tsync_supported = *TEST_ABI >= ABI::V8;
687
688        for all_threads in [false, true] {
689            // The sibling is covered only when TSYNC is requested and supported.
690            let expect_sibling_denied = all_threads && tsync_supported;
691
692            match unsafe { libc::fork() } {
693                -1 => panic!("fork() failed: {}", std::io::Error::last_os_error()),
694                0 => {
695                    // Any panic (builder error, spawn/join failure) becomes a
696                    // non-zero code rather than being swallowed as a pass.
697                    let code = std::panic::catch_unwind(|| {
698                        let barrier = Arc::new(Barrier::new(2));
699                        let sibling = {
700                            let barrier = Arc::clone(&barrier);
701                            thread::spawn(move || {
702                                let before = can_list_root();
703                                // Both threads probed before enforcing.
704                                barrier.wait();
705                                // Main has called restrict_self().
706                                barrier.wait();
707                                (before, can_list_root())
708                            })
709                        };
710
711                        let main_before = can_list_root();
712                        barrier.wait();
713
714                        let status = Ruleset::default()
715                            .handle_access(AccessFs::from_all(ABI::V1))
716                            .unwrap()
717                            .create()
718                            .unwrap()
719                            .all_threads(all_threads)
720                            .unwrap()
721                            .restrict_self()
722                            .unwrap();
723                        barrier.wait();
724
725                        let main_denied = !can_list_root();
726                        let (sibling_before, sibling_after) = sibling.join().unwrap();
727                        let sibling_denied = !sibling_after;
728                        let ruleset_enforced = status.ruleset != RulesetStatus::NotEnforced;
729
730                        let mut code = OK;
731                        // Both threads can list "/" before enforcement.
732                        if !main_before {
733                            code |= MAIN_BEFORE_DENIED;
734                        }
735                        if !sibling_before {
736                            code |= SIBLING_BEFORE_DENIED;
737                        }
738                        // Kernel reality matches the independent expectation.
739                        if main_denied != landlock_supported {
740                            code |= MAIN_AFTER_MISMATCH;
741                        }
742                        if sibling_denied != expect_sibling_denied {
743                            code |= SIBLING_AFTER_MISMATCH;
744                        }
745                        // The crate's reported status matches it too.
746                        if ruleset_enforced != landlock_supported {
747                            code |= STATUS_RULESET_MISMATCH;
748                        }
749                        if status.all_threads != expect_sibling_denied {
750                            code |= STATUS_TSYNC_MISMATCH;
751                        }
752                        code
753                    })
754                    .unwrap_or(PANICKED);
755                    // _exit avoids atexit handlers and destructors inherited from
756                    // the test harness.
757                    unsafe { libc::_exit(code) };
758                }
759                pid => {
760                    let mut wstatus: libc::c_int = 0;
761                    let ret = unsafe { libc::waitpid(pid, &mut wstatus, 0) };
762                    assert_eq!(ret, pid);
763                    assert!(libc::WIFEXITED(wstatus));
764                    assert_eq!(libc::WEXITSTATUS(wstatus), OK, "all_threads({all_threads})");
765                }
766            }
767        }
768    }
769
770    #[test]
771    fn abi_v9_resolve_unix() {
772        // ResolveUnix is the only access right added in ABI v9, and ABI v8 added
773        // no filesystem access right, so there is no access from a lower ABI to
774        // combine it with while keeping `partial` and `full` adjacent.  It is
775        // therefore tested on its own with partial == full == V9, like
776        // abi_v2_refer_only.  Pairing it with a non-adjacent lower right (e.g.
777        // IoctlDev from V5) would leave mocked ABIs V6..=V8 where only that lower
778        // right is enforced (PartiallyEnforced); can_emulate() expects an error
779        // whenever the runner kernel's ABI is below the mocked one, so such a
780        // test would pass or fail depending on the runner kernel.
781        //
782        // For a mocked ABI below V9, ResolveUnix is dropped by best-effort
783        // compatibility: no access is handled, create() builds no kernel ruleset
784        // and add_rule() is a no-op, so restrict_self() reports NotEnforced (not
785        // an error).  At mocked ABI V9 the ruleset is fully enforced on a v9
786        // kernel, or the create()/add_rule() syscall is rejected on an older one
787        // (asserted through can_emulate()'s error path).
788        check_ruleset_support(
789            ABI::V9,
790            Some(ABI::V9),
791            move |ruleset: Ruleset| -> _ {
792                Ok(ruleset
793                    .handle_access(AccessFs::ResolveUnix)?
794                    .create()?
795                    .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::ResolveUnix))?
796                    .restrict_self()?)
797            },
798            false,
799        );
800    }
801
802    #[test]
803    fn abi_v10_udp() {
804        // ResolveUnix keeps ABI v9 partially enforced before UDP support in v10.
805        check_ruleset_support(
806            ABI::V9,
807            Some(ABI::V10),
808            move |ruleset: Ruleset| -> _ {
809                Ok(ruleset
810                    .handle_access(AccessFs::ResolveUnix)?
811                    .handle_access(AccessNet::BindUdp | AccessNet::ConnectSendUdp)?
812                    .create()?
813                    .add_rule(NetPort::new(1, AccessNet::ConnectSendUdp))?
814                    .restrict_self()?)
815            },
816            false,
817        );
818    }
819}