Skip to main content

landlock/
fs.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2
3use crate::compat::private::OptionCompatLevelMut;
4use crate::{
5    uapi, Access, AddRuleError, AddRulesError, CompatError, CompatLevel, CompatResult, CompatState,
6    Compatible, HandleAccessError, HandleAccessesError, HandledAccess, PathBeneathError,
7    PathFdError, PrivateHandledAccess, PrivateRule, Rule, Ruleset, RulesetCreated, RulesetError,
8    TailoredCompatLevel, TryCompat, ABI,
9};
10use enumflags2::{bitflags, make_bitflags, BitFlags};
11use std::fs::OpenOptions;
12use std::io::Error;
13use std::mem::zeroed;
14use std::os::unix::fs::OpenOptionsExt;
15use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
16use std::path::Path;
17
18#[cfg(test)]
19use crate::{RulesetAttr, RulesetCreatedAttr};
20#[cfg(test)]
21use strum::IntoEnumIterator;
22
23/// File system access right.
24///
25/// Each variant of `AccessFs` is an [access right](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights)
26/// for the file system.
27/// A set of access rights can be created with [`BitFlags<AccessFs>`](BitFlags).
28///
29/// # Example
30///
31/// ```
32/// use landlock::{ABI, Access, AccessFs, BitFlags, make_bitflags};
33///
34/// let exec = AccessFs::Execute;
35///
36/// let exec_set: BitFlags<AccessFs> = exec.into();
37///
38/// let file_content = make_bitflags!(AccessFs::{Execute | WriteFile | ReadFile});
39///
40/// let fs_v1 = AccessFs::from_all(ABI::V1);
41///
42/// let without_exec = fs_v1 & !AccessFs::Execute;
43///
44/// assert_eq!(fs_v1 | AccessFs::Refer, AccessFs::from_all(ABI::V2));
45/// ```
46///
47/// # Warning
48///
49/// To avoid unknown restrictions **don't use `BitFlags::<AccessFs>::all()` nor `BitFlags::ALL`**,
50/// but use a version you tested and vetted instead,
51/// for instance [`AccessFs::from_all(ABI::V1)`](Access::from_all).
52/// Direct use of **the [`BitFlags`] API is deprecated**.
53/// See [`ABI`] for the rationale and help to test it.
54#[bitflags]
55#[repr(u64)]
56#[derive(Copy, Clone, Debug, PartialEq, Eq)]
57#[non_exhaustive]
58pub enum AccessFs {
59    /// Execute a file.
60    Execute = uapi::LANDLOCK_ACCESS_FS_EXECUTE as u64,
61    /// Open a file with write access.
62    ///
63    /// # Note
64    ///
65    /// Certain operations (such as [`std::fs::write`]) may also require [`AccessFs::Truncate`] since [`ABI::V3`].
66    WriteFile = uapi::LANDLOCK_ACCESS_FS_WRITE_FILE as u64,
67    /// Open a file with read access.
68    ReadFile = uapi::LANDLOCK_ACCESS_FS_READ_FILE as u64,
69    /// Open a directory or list its content.
70    ReadDir = uapi::LANDLOCK_ACCESS_FS_READ_DIR as u64,
71    /// Remove an empty directory or rename one.
72    RemoveDir = uapi::LANDLOCK_ACCESS_FS_REMOVE_DIR as u64,
73    /// Unlink (or rename) a file.
74    RemoveFile = uapi::LANDLOCK_ACCESS_FS_REMOVE_FILE as u64,
75    /// Create (or rename or link) a character device.
76    MakeChar = uapi::LANDLOCK_ACCESS_FS_MAKE_CHAR as u64,
77    /// Create (or rename) a directory.
78    MakeDir = uapi::LANDLOCK_ACCESS_FS_MAKE_DIR as u64,
79    /// Create (or rename or link) a regular file.
80    MakeReg = uapi::LANDLOCK_ACCESS_FS_MAKE_REG as u64,
81    /// Create (or rename or link) a UNIX domain socket.
82    MakeSock = uapi::LANDLOCK_ACCESS_FS_MAKE_SOCK as u64,
83    /// Create (or rename or link) a named pipe.
84    MakeFifo = uapi::LANDLOCK_ACCESS_FS_MAKE_FIFO as u64,
85    /// Create (or rename or link) a block device.
86    MakeBlock = uapi::LANDLOCK_ACCESS_FS_MAKE_BLOCK as u64,
87    /// Create (or rename or link) a symbolic link.
88    MakeSym = uapi::LANDLOCK_ACCESS_FS_MAKE_SYM as u64,
89    /// Link or rename a file from or to a different directory.
90    Refer = uapi::LANDLOCK_ACCESS_FS_REFER as u64,
91    /// Truncate a file with `truncate(2)`, `ftruncate(2)`, `creat(2)`, or `open(2)` with `O_TRUNC`.
92    Truncate = uapi::LANDLOCK_ACCESS_FS_TRUNCATE as u64,
93    /// Send IOCL commands to a device file.
94    IoctlDev = uapi::LANDLOCK_ACCESS_FS_IOCTL_DEV as u64,
95    /// Connect to a pathname UNIX domain socket with `connect(2)` or `sendmsg(2)`.
96    ResolveUnix = uapi::LANDLOCK_ACCESS_FS_RESOLVE_UNIX as u64,
97}
98
99impl Access for AccessFs {
100    /// Union of [`from_read()`](AccessFs::from_read) and [`from_write()`](AccessFs::from_write).
101    fn from_all(abi: ABI) -> BitFlags<Self> {
102        // An empty access-right would be an error if passed to the kernel, but because the kernel
103        // doesn't support Landlock, no Landlock syscall should be called.  try_compat() should
104        // also return RestrictionStatus::Unrestricted when called with unsupported/empty
105        // access-rights.
106        Self::from_read(abi) | Self::from_write(abi)
107    }
108}
109
110impl AccessFs {
111    // Roughly read (i.e. not all FS actions are handled).
112    /// Gets the access rights identified as read-only according to a specific ABI.
113    /// Exclusive with [`from_write()`](AccessFs::from_write).
114    pub fn from_read(abi: ABI) -> BitFlags<Self> {
115        match abi {
116            ABI::Unsupported => BitFlags::EMPTY,
117            ABI::V1
118            | ABI::V2
119            | ABI::V3
120            | ABI::V4
121            | ABI::V5
122            | ABI::V6
123            | ABI::V7
124            | ABI::V8
125            | ABI::V9 => {
126                make_bitflags!(AccessFs::{
127                    Execute
128                    | ReadFile
129                    | ReadDir
130                })
131            }
132        }
133    }
134
135    // Roughly write (i.e. not all FS actions are handled).
136    /// Gets the access rights identified as write-only according to a specific ABI.
137    /// Exclusive with [`from_read()`](AccessFs::from_read).
138    pub fn from_write(abi: ABI) -> BitFlags<Self> {
139        match abi {
140            ABI::Unsupported => BitFlags::EMPTY,
141            ABI::V1 => make_bitflags!(AccessFs::{
142                WriteFile
143                | RemoveDir
144                | RemoveFile
145                | MakeChar
146                | MakeDir
147                | MakeReg
148                | MakeSock
149                | MakeFifo
150                | MakeBlock
151                | MakeSym
152            }),
153            ABI::V2 => Self::from_write(ABI::V1) | AccessFs::Refer,
154            ABI::V3 | ABI::V4 => Self::from_write(ABI::V2) | AccessFs::Truncate,
155            ABI::V5 | ABI::V6 | ABI::V7 | ABI::V8 => Self::from_write(ABI::V4) | AccessFs::IoctlDev,
156            ABI::V9 => Self::from_write(ABI::V8) | AccessFs::ResolveUnix,
157        }
158    }
159
160    /// Gets the access rights legitimate for non-directory files.
161    pub fn from_file(abi: ABI) -> BitFlags<Self> {
162        Self::from_all(abi) & ACCESS_FILE
163    }
164}
165
166#[test]
167fn consistent_access_fs_rw() {
168    for abi in ABI::iter() {
169        let access_all = AccessFs::from_all(abi);
170        let access_read = AccessFs::from_read(abi);
171        let access_write = AccessFs::from_write(abi);
172        let access_file = AccessFs::from_file(abi);
173        assert_eq!(access_read, !access_write & access_all);
174        assert_eq!(access_read | access_write, access_all);
175        assert_eq!(access_file, access_all & ACCESS_FILE);
176    }
177}
178
179impl HandledAccess for AccessFs {}
180
181impl PrivateHandledAccess for AccessFs {
182    fn ruleset_handle_access(
183        ruleset: &mut Ruleset,
184        access: BitFlags<Self>,
185    ) -> Result<(), HandleAccessesError> {
186        // We need to record the requested accesses for PrivateRule::check_consistency().
187        ruleset.requested_handled_fs |= access;
188        ruleset.actual_handled_fs |= match access
189            .try_compat(
190                ruleset.compat.abi(),
191                ruleset.compat.level,
192                &mut ruleset.compat.state,
193            )
194            .map_err(HandleAccessError::Compat)?
195        {
196            Some(a) => a,
197            None => return Ok(()),
198        };
199        Ok(())
200    }
201
202    fn into_add_rules_error(error: AddRuleError<Self>) -> AddRulesError {
203        AddRulesError::Fs(error)
204    }
205
206    fn into_handle_accesses_error(error: HandleAccessError<Self>) -> HandleAccessesError {
207        HandleAccessesError::Fs(error)
208    }
209}
210
211// TODO: Make ACCESS_FILE a property of AccessFs.
212// TODO: Add tests for ACCESS_FILE.
213const ACCESS_FILE: BitFlags<AccessFs> = make_bitflags!(AccessFs::{
214    ReadFile | WriteFile | Execute | Truncate | IoctlDev | ResolveUnix
215});
216
217// XXX: What should we do when a stat call failed?
218fn is_file<F>(fd: F) -> Result<bool, Error>
219where
220    F: AsFd,
221{
222    unsafe {
223        let mut stat = zeroed();
224        match libc::fstat(fd.as_fd().as_raw_fd(), &mut stat) {
225            0 => Ok((stat.st_mode & libc::S_IFMT) != libc::S_IFDIR),
226            _ => Err(Error::last_os_error()),
227        }
228    }
229}
230
231/// Landlock rule for a file hierarchy.
232///
233/// # Example
234///
235/// ```
236/// use landlock::{AccessFs, PathBeneath, PathFd, PathFdError};
237///
238/// fn home_dir() -> Result<PathBeneath<PathFd>, PathFdError> {
239///     Ok(PathBeneath::new(PathFd::new("/home")?, AccessFs::ReadDir))
240/// }
241/// ```
242#[derive(Debug)]
243pub struct PathBeneath<F> {
244    attr: uapi::landlock_path_beneath_attr,
245    // Ties the lifetime of a file descriptor to this object.
246    parent_fd: F,
247    allowed_access: BitFlags<AccessFs>,
248    compat_level: Option<CompatLevel>,
249}
250
251impl<F> PathBeneath<F>
252where
253    F: AsFd,
254{
255    /// Creates a new `PathBeneath` rule identifying the `parent` directory of a file hierarchy,
256    /// or just a file, and allows `access` on it.
257    /// The `parent` file descriptor will be automatically closed with the returned `PathBeneath`.
258    pub fn new<A>(parent: F, access: A) -> Self
259    where
260        A: Into<BitFlags<AccessFs>>,
261    {
262        PathBeneath {
263            // Invalid access rights until as_ptr() is called.
264            attr: unsafe { zeroed() },
265            parent_fd: parent,
266            allowed_access: access.into(),
267            compat_level: None,
268        }
269    }
270}
271
272impl<F> TryCompat<AccessFs> for PathBeneath<F>
273where
274    F: AsFd,
275{
276    fn try_compat_children<L>(
277        mut self,
278        abi: ABI,
279        parent_level: L,
280        compat_state: &mut CompatState,
281    ) -> Result<Option<Self>, CompatError<AccessFs>>
282    where
283        L: Into<CompatLevel>,
284    {
285        // Checks with our own compatibility level, if any.
286        self.allowed_access = match self.allowed_access.try_compat(
287            abi,
288            self.tailored_compat_level(parent_level),
289            compat_state,
290        )? {
291            Some(a) => a,
292            None => return Ok(None),
293        };
294        Ok(Some(self))
295    }
296
297    fn try_compat_inner(
298        &mut self,
299        _abi: ABI,
300    ) -> Result<CompatResult<AccessFs>, CompatError<AccessFs>> {
301        // Gets subset of valid accesses according the FD type.
302        let valid_access =
303            if is_file(&self.parent_fd).map_err(|e| PathBeneathError::StatCall { source: e })? {
304                self.allowed_access & ACCESS_FILE
305            } else {
306                self.allowed_access
307            };
308
309        if self.allowed_access != valid_access {
310            let error = PathBeneathError::DirectoryAccess {
311                access: self.allowed_access,
312                incompatible: self.allowed_access ^ valid_access,
313            }
314            .into();
315            self.allowed_access = valid_access;
316            // Linux would return EINVAL.
317            Ok(CompatResult::Partial(error))
318        } else {
319            Ok(CompatResult::Full)
320        }
321    }
322}
323
324#[test]
325fn path_beneath_try_compat_children() {
326    use crate::*;
327
328    // AccessFs::Refer is not handled by ABI::V1 and only for directories.
329    let access_file = AccessFs::ReadFile | AccessFs::Refer;
330
331    // Test error ordering with ABI::V1
332    let mut ruleset = Ruleset::from(ABI::V1).handle_access(access_file).unwrap();
333    // Do not actually perform any syscall.
334    ruleset.compat.state = CompatState::Dummy;
335    assert!(matches!(
336        RulesetCreated::new(ruleset, None)
337            .set_compatibility(CompatLevel::HardRequirement)
338            .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
339            .unwrap_err(),
340        RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
341            CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
342        ))) if access == access_file && incompatible == AccessFs::Refer
343    ));
344
345    // Test error ordering with ABI::V2
346    let mut ruleset = Ruleset::from(ABI::V2).handle_access(access_file).unwrap();
347    // Do not actually perform any syscall.
348    ruleset.compat.state = CompatState::Dummy;
349    assert!(matches!(
350        RulesetCreated::new(ruleset, None)
351            .set_compatibility(CompatLevel::HardRequirement)
352            .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
353            .unwrap_err(),
354        RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
355            CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
356        ))) if access == access_file && incompatible == AccessFs::Refer
357    ));
358}
359
360#[test]
361fn path_beneath_try_compat() {
362    use crate::*;
363
364    let abi = ABI::V1;
365
366    for file in &["/etc/passwd", "/dev/null"] {
367        let mut compat_state = CompatState::Init;
368        let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
369        assert!(matches!(
370            PathBeneath::new(PathFd::new(file).unwrap(), ro_access)
371                .try_compat(abi, CompatLevel::HardRequirement, &mut compat_state)
372                .unwrap_err(),
373            CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
374                if access == ro_access && incompatible == AccessFs::ReadDir
375        ));
376
377        let mut compat_state = CompatState::Init;
378        assert!(matches!(
379            PathBeneath::new(PathFd::new(file).unwrap(), BitFlags::EMPTY)
380                .try_compat(abi, CompatLevel::BestEffort, &mut compat_state)
381                .unwrap_err(),
382            CompatError::Access(AccessError::Empty)
383        ));
384    }
385
386    let full_access = AccessFs::from_all(ABI::V1);
387    for compat_level in &[
388        CompatLevel::BestEffort,
389        CompatLevel::SoftRequirement,
390        CompatLevel::HardRequirement,
391    ] {
392        let mut compat_state = CompatState::Init;
393        let mut path_beneath = PathBeneath::new(PathFd::new("/").unwrap(), full_access)
394            .try_compat(abi, *compat_level, &mut compat_state)
395            .unwrap()
396            .unwrap();
397        assert_eq!(compat_state, CompatState::Full);
398
399        // Without synchronization.
400        let raw_access = path_beneath.attr.allowed_access;
401        assert_eq!(raw_access, 0);
402
403        // Synchronize the inner attribute buffer.
404        let _ = path_beneath.as_ptr();
405        let raw_access = path_beneath.attr.allowed_access;
406        assert_eq!(raw_access, full_access.bits());
407    }
408}
409
410impl<F> OptionCompatLevelMut for PathBeneath<F> {
411    fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
412        &mut self.compat_level
413    }
414}
415
416impl<F> OptionCompatLevelMut for &mut PathBeneath<F> {
417    fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
418        &mut self.compat_level
419    }
420}
421
422impl<F> Compatible for PathBeneath<F> {}
423
424impl<F> Compatible for &mut PathBeneath<F> {}
425
426#[test]
427fn path_beneath_compatibility() {
428    let mut path = PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::from_all(ABI::V1));
429    let path_ref = &mut path;
430
431    let level = path_ref.as_option_compat_level_mut();
432    assert_eq!(level, &None);
433    assert_eq!(
434        <Option<CompatLevel> as Into<CompatLevel>>::into(*level),
435        CompatLevel::BestEffort
436    );
437
438    path_ref.set_compatibility(CompatLevel::SoftRequirement);
439    assert_eq!(
440        path_ref.as_option_compat_level_mut(),
441        &Some(CompatLevel::SoftRequirement)
442    );
443
444    path.set_compatibility(CompatLevel::HardRequirement);
445}
446
447// It is useful for documentation generation to explicitely implement Rule for every types, instead
448// of doing it generically.
449impl<F> Rule<AccessFs> for PathBeneath<F> where F: AsFd {}
450
451impl<F> PrivateRule<AccessFs> for PathBeneath<F>
452where
453    F: AsFd,
454{
455    const TYPE_ID: uapi::landlock_rule_type = uapi::landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH;
456
457    fn as_ptr(&mut self) -> *const libc::c_void {
458        self.attr.parent_fd = self.parent_fd.as_fd().as_raw_fd();
459        self.attr.allowed_access = self.allowed_access.bits();
460        &self.attr as *const _ as _
461    }
462
463    fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError> {
464        // Checks that this rule doesn't contain a superset of the access-rights handled by the
465        // ruleset.  This check is about requested access-rights but not actual access-rights.
466        // Indeed, we want to get a deterministic behavior, i.e. not based on the running kernel
467        // (which is handled by Ruleset and RulesetCreated).
468        if ruleset.requested_handled_fs.contains(self.allowed_access) {
469            Ok(())
470        } else {
471            Err(AddRuleError::UnhandledAccess {
472                access: self.allowed_access,
473                incompatible: self.allowed_access & !ruleset.requested_handled_fs,
474            }
475            .into())
476        }
477    }
478}
479
480#[test]
481fn path_beneath_check_consistency() {
482    use crate::*;
483
484    let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
485    let rx_access = AccessFs::Execute | AccessFs::ReadFile;
486    assert!(matches!(
487        Ruleset::from(ABI::Unsupported)
488            .handle_access(ro_access)
489            .unwrap()
490            .create()
491            .unwrap()
492            .add_rule(PathBeneath::new(PathFd::new("/").unwrap(), rx_access))
493            .unwrap_err(),
494        RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { access, incompatible }))
495            if access == rx_access && incompatible == AccessFs::Execute
496    ));
497}
498
499/// Simple helper to open a file or a directory with the `O_PATH` flag.
500///
501/// This is the recommended way to identify a path
502/// and manage the lifetime of the underlying opened file descriptor.
503/// Indeed, using other [`AsFd`] implementations such as [`File`] brings more complexity
504/// and may lead to unexpected errors (e.g., denied access).
505///
506/// [`File`]: std::fs::File
507///
508/// # Example
509///
510/// ```
511/// use landlock::{AccessFs, PathBeneath, PathFd, PathFdError};
512///
513/// fn allowed_root_dir(access: AccessFs) -> Result<PathBeneath<PathFd>, PathFdError> {
514///     let fd = PathFd::new("/")?;
515///     Ok(PathBeneath::new(fd, access))
516/// }
517/// ```
518#[derive(Debug)]
519pub struct PathFd {
520    fd: OwnedFd,
521}
522
523impl PathFd {
524    pub fn new<T>(path: T) -> Result<Self, PathFdError>
525    where
526        T: AsRef<Path>,
527    {
528        Ok(PathFd {
529            fd: OpenOptions::new()
530                .read(true)
531                // If the O_PATH is not supported, it is automatically ignored (Linux < 2.6.39).
532                .custom_flags(libc::O_PATH | libc::O_CLOEXEC)
533                .open(path.as_ref())
534                .map_err(|e| PathFdError::OpenCall {
535                    source: e,
536                    path: path.as_ref().into(),
537                })?
538                .into(),
539        })
540    }
541}
542
543impl AsFd for PathFd {
544    fn as_fd(&self) -> BorrowedFd<'_> {
545        self.fd.as_fd()
546    }
547}
548
549#[test]
550fn path_fd() {
551    use std::fs::File;
552    use std::io::Read;
553
554    PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Execute);
555    PathBeneath::new(File::open("/").unwrap(), AccessFs::Execute);
556
557    let mut buffer = [0; 1];
558    // Checks that PathFd really returns an FD opened with O_PATH (Bad file descriptor error).
559    File::from(PathFd::new("/etc/passwd").unwrap().fd)
560        .read(&mut buffer)
561        .unwrap_err();
562}
563
564/// Helper to quickly create an iterator of PathBeneath rules.
565///
566/// # Note
567///
568/// From the kernel's perspective, Landlock rules operate on file descriptors, not paths.
569/// This is a helper to create rules based on paths. Here, `path_beneath_rules()` silently ignores
570/// paths that cannot be opened, hence making the obtainment of a file descriptor impossible. When
571/// possible and for a given path, `path_beneath_rules()` automatically adjusts [access rights](`AccessFs`),
572/// depending on whether a directory or a file is present at that said path.
573///
574/// This behavior is the result of [`CompatLevel::BestEffort`], which is the default compatibility level of
575/// all created rulesets. Thus, it applies to the example below. However, if [`CompatLevel::HardRequirement`]
576/// is set using [`Compatible::set_compatibility`], attempting to create an incompatible rule at runtime will cause
577/// this crate to raise an error instead.
578///
579/// # Example
580///
581/// ```
582/// use landlock::{
583///     ABI, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetStatus, RulesetError,
584///     path_beneath_rules,
585/// };
586///
587/// fn restrict_thread() -> Result<(), RulesetError> {
588///     let abi = ABI::V1;
589///     let status = Ruleset::default()
590///         .handle_access(AccessFs::from_all(abi))?
591///         .create()?
592///         // Read-only access to /usr, /etc and /dev.
593///         .add_rules(path_beneath_rules(&["/usr", "/etc", "/dev"], AccessFs::from_read(abi)))?
594///         // Read-write access to /home and /tmp.
595///         .add_rules(path_beneath_rules(&["/home", "/tmp"], AccessFs::from_all(abi)))?
596///         .restrict_self()?;
597///     match status.ruleset {
598///         // The FullyEnforced case must be tested by the developer.
599///         RulesetStatus::FullyEnforced => println!("Fully sandboxed."),
600///         RulesetStatus::PartiallyEnforced => println!("Partially sandboxed."),
601///         // Users should be warned that they are not protected.
602///         RulesetStatus::NotEnforced => println!("Not sandboxed! Please update your kernel."),
603///     }
604///     Ok(())
605/// }
606/// ```
607pub fn path_beneath_rules<I, P, A>(
608    paths: I,
609    access: A,
610) -> impl Iterator<Item = Result<PathBeneath<PathFd>, RulesetError>>
611where
612    I: IntoIterator<Item = P>,
613    P: AsRef<Path>,
614    A: Into<BitFlags<AccessFs>>,
615{
616    let access = access.into();
617    paths.into_iter().filter_map(move |p| match PathFd::new(p) {
618        Ok(f) => {
619            let valid_access = match is_file(&f) {
620                Ok(true) => access & ACCESS_FILE,
621                // If the stat call failed, let's blindly rely on the requested access rights.
622                Err(_) | Ok(false) => access,
623            };
624            Some(Ok(PathBeneath::new(f, valid_access)))
625        }
626        Err(_) => None,
627    })
628}
629
630#[test]
631fn path_beneath_rules_iter() {
632    let _ = Ruleset::default()
633        .handle_access(AccessFs::from_all(ABI::V1))
634        .unwrap()
635        .create()
636        .unwrap()
637        .add_rules(path_beneath_rules(
638            &["/usr", "/opt", "/does-not-exist", "/root"],
639            AccessFs::Execute,
640        ))
641        .unwrap();
642}