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            | ABI::V10 => {
127                make_bitflags!(AccessFs::{
128                    Execute
129                    | ReadFile
130                    | ReadDir
131                })
132            }
133        }
134    }
135
136    // Roughly write (i.e. not all FS actions are handled).
137    /// Gets the access rights identified as write-only according to a specific ABI.
138    /// Exclusive with [`from_read()`](AccessFs::from_read).
139    pub fn from_write(abi: ABI) -> BitFlags<Self> {
140        match abi {
141            ABI::Unsupported => BitFlags::EMPTY,
142            ABI::V1 => make_bitflags!(AccessFs::{
143                WriteFile
144                | RemoveDir
145                | RemoveFile
146                | MakeChar
147                | MakeDir
148                | MakeReg
149                | MakeSock
150                | MakeFifo
151                | MakeBlock
152                | MakeSym
153            }),
154            ABI::V2 => Self::from_write(ABI::V1) | AccessFs::Refer,
155            ABI::V3 | ABI::V4 => Self::from_write(ABI::V2) | AccessFs::Truncate,
156            ABI::V5 | ABI::V6 | ABI::V7 | ABI::V8 => Self::from_write(ABI::V4) | AccessFs::IoctlDev,
157            ABI::V9 | ABI::V10 => Self::from_write(ABI::V8) | AccessFs::ResolveUnix,
158        }
159    }
160
161    /// Gets the access rights legitimate for non-directory files.
162    pub fn from_file(abi: ABI) -> BitFlags<Self> {
163        Self::from_all(abi) & ACCESS_FILE
164    }
165}
166
167#[test]
168fn consistent_access_fs_rw() {
169    for abi in ABI::iter() {
170        let access_all = AccessFs::from_all(abi);
171        let access_read = AccessFs::from_read(abi);
172        let access_write = AccessFs::from_write(abi);
173        let access_file = AccessFs::from_file(abi);
174        assert_eq!(access_read, !access_write & access_all);
175        assert_eq!(access_read | access_write, access_all);
176        assert_eq!(access_file, access_all & ACCESS_FILE);
177    }
178}
179
180impl HandledAccess for AccessFs {}
181
182impl PrivateHandledAccess for AccessFs {
183    fn ruleset_handle_access(
184        ruleset: &mut Ruleset,
185        access: BitFlags<Self>,
186    ) -> Result<(), HandleAccessesError> {
187        // We need to record the requested accesses for PrivateRule::check_consistency().
188        ruleset.requested_handled_fs |= access;
189        ruleset.actual_handled_fs |= match access
190            .try_compat(
191                ruleset.compat.abi(),
192                ruleset.compat.level,
193                &mut ruleset.compat.state,
194            )
195            .map_err(HandleAccessError::Compat)?
196        {
197            Some(a) => a,
198            None => return Ok(()),
199        };
200        Ok(())
201    }
202
203    fn into_add_rules_error(error: AddRuleError<Self>) -> AddRulesError {
204        AddRulesError::Fs(error)
205    }
206
207    fn into_handle_accesses_error(error: HandleAccessError<Self>) -> HandleAccessesError {
208        HandleAccessesError::Fs(error)
209    }
210}
211
212// TODO: Make ACCESS_FILE a property of AccessFs.
213// TODO: Add tests for ACCESS_FILE.
214const ACCESS_FILE: BitFlags<AccessFs> = make_bitflags!(AccessFs::{
215    ReadFile | WriteFile | Execute | Truncate | IoctlDev | ResolveUnix
216});
217
218// XXX: What should we do when a stat call failed?
219fn is_file<F>(fd: F) -> Result<bool, Error>
220where
221    F: AsFd,
222{
223    unsafe {
224        let mut stat = zeroed();
225        match libc::fstat(fd.as_fd().as_raw_fd(), &mut stat) {
226            0 => Ok((stat.st_mode & libc::S_IFMT) != libc::S_IFDIR),
227            _ => Err(Error::last_os_error()),
228        }
229    }
230}
231
232/// Landlock rule for a file hierarchy.
233///
234/// # Example
235///
236/// ```
237/// use landlock::{AccessFs, PathBeneath, PathFd, PathFdError};
238///
239/// fn home_dir() -> Result<PathBeneath<PathFd>, PathFdError> {
240///     Ok(PathBeneath::new(PathFd::new("/home")?, AccessFs::ReadDir))
241/// }
242/// ```
243#[derive(Debug)]
244pub struct PathBeneath<F> {
245    attr: uapi::landlock_path_beneath_attr,
246    // Ties the lifetime of a file descriptor to this object.
247    parent_fd: F,
248    allowed_access: BitFlags<AccessFs>,
249    compat_level: Option<CompatLevel>,
250}
251
252impl<F> PathBeneath<F>
253where
254    F: AsFd,
255{
256    /// Creates a new `PathBeneath` rule identifying the `parent` directory of a file hierarchy,
257    /// or just a file, and allows `access` on it.
258    /// The `parent` file descriptor will be automatically closed with the returned `PathBeneath`.
259    pub fn new<A>(parent: F, access: A) -> Self
260    where
261        A: Into<BitFlags<AccessFs>>,
262    {
263        PathBeneath {
264            // Invalid access rights until as_ptr() is called.
265            attr: unsafe { zeroed() },
266            parent_fd: parent,
267            allowed_access: access.into(),
268            compat_level: None,
269        }
270    }
271}
272
273impl<F> TryCompat<AccessFs> for PathBeneath<F>
274where
275    F: AsFd,
276{
277    fn try_compat_children<L>(
278        mut self,
279        abi: ABI,
280        parent_level: L,
281        compat_state: &mut CompatState,
282    ) -> Result<Option<Self>, CompatError<AccessFs>>
283    where
284        L: Into<CompatLevel>,
285    {
286        // Checks with our own compatibility level, if any.
287        self.allowed_access = match self.allowed_access.try_compat(
288            abi,
289            self.tailored_compat_level(parent_level),
290            compat_state,
291        )? {
292            Some(a) => a,
293            None => return Ok(None),
294        };
295        Ok(Some(self))
296    }
297
298    fn try_compat_inner(
299        &mut self,
300        _abi: ABI,
301    ) -> Result<CompatResult<AccessFs>, CompatError<AccessFs>> {
302        // Gets subset of valid accesses according the FD type.
303        let valid_access =
304            if is_file(&self.parent_fd).map_err(|e| PathBeneathError::StatCall { source: e })? {
305                self.allowed_access & ACCESS_FILE
306            } else {
307                self.allowed_access
308            };
309
310        if self.allowed_access != valid_access {
311            let error = PathBeneathError::DirectoryAccess {
312                access: self.allowed_access,
313                incompatible: self.allowed_access ^ valid_access,
314            }
315            .into();
316            self.allowed_access = valid_access;
317            // Linux would return EINVAL.
318            Ok(CompatResult::Partial(error))
319        } else {
320            Ok(CompatResult::Full)
321        }
322    }
323}
324
325#[test]
326fn path_beneath_try_compat_children() {
327    use crate::*;
328
329    // AccessFs::Refer is not handled by ABI::V1 and only for directories.
330    let access_file = AccessFs::ReadFile | AccessFs::Refer;
331
332    // Test error ordering with ABI::V1
333    let mut ruleset = Ruleset::from(ABI::V1).handle_access(access_file).unwrap();
334    // Do not actually perform any syscall.
335    ruleset.compat.state = CompatState::Dummy;
336    assert!(matches!(
337        RulesetCreated::new(ruleset, None)
338            .set_compatibility(CompatLevel::HardRequirement)
339            .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
340            .unwrap_err(),
341        RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
342            CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
343        ))) if access == access_file && incompatible == AccessFs::Refer
344    ));
345
346    // Test error ordering with ABI::V2
347    let mut ruleset = Ruleset::from(ABI::V2).handle_access(access_file).unwrap();
348    // Do not actually perform any syscall.
349    ruleset.compat.state = CompatState::Dummy;
350    assert!(matches!(
351        RulesetCreated::new(ruleset, None)
352            .set_compatibility(CompatLevel::HardRequirement)
353            .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
354            .unwrap_err(),
355        RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
356            CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
357        ))) if access == access_file && incompatible == AccessFs::Refer
358    ));
359}
360
361#[test]
362fn path_beneath_try_compat() {
363    use crate::*;
364
365    let abi = ABI::V1;
366
367    for file in &["/etc/passwd", "/dev/null"] {
368        let mut compat_state = CompatState::Init;
369        let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
370        assert!(matches!(
371            PathBeneath::new(PathFd::new(file).unwrap(), ro_access)
372                .try_compat(abi, CompatLevel::HardRequirement, &mut compat_state)
373                .unwrap_err(),
374            CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
375                if access == ro_access && incompatible == AccessFs::ReadDir
376        ));
377
378        let mut compat_state = CompatState::Init;
379        assert!(matches!(
380            PathBeneath::new(PathFd::new(file).unwrap(), BitFlags::EMPTY)
381                .try_compat(abi, CompatLevel::BestEffort, &mut compat_state)
382                .unwrap_err(),
383            CompatError::Access(AccessError::Empty)
384        ));
385    }
386
387    let full_access = AccessFs::from_all(ABI::V1);
388    for compat_level in &[
389        CompatLevel::BestEffort,
390        CompatLevel::SoftRequirement,
391        CompatLevel::HardRequirement,
392    ] {
393        let mut compat_state = CompatState::Init;
394        let mut path_beneath = PathBeneath::new(PathFd::new("/").unwrap(), full_access)
395            .try_compat(abi, *compat_level, &mut compat_state)
396            .unwrap()
397            .unwrap();
398        assert_eq!(compat_state, CompatState::Full);
399
400        // Without synchronization.
401        let raw_access = path_beneath.attr.allowed_access;
402        assert_eq!(raw_access, 0);
403
404        // Synchronize the inner attribute buffer.
405        let _ = path_beneath.as_ptr();
406        let raw_access = path_beneath.attr.allowed_access;
407        assert_eq!(raw_access, full_access.bits());
408    }
409}
410
411impl<F> OptionCompatLevelMut for PathBeneath<F> {
412    fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
413        &mut self.compat_level
414    }
415}
416
417impl<F> OptionCompatLevelMut for &mut PathBeneath<F> {
418    fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
419        &mut self.compat_level
420    }
421}
422
423impl<F> Compatible for PathBeneath<F> {}
424
425impl<F> Compatible for &mut PathBeneath<F> {}
426
427#[test]
428fn path_beneath_compatibility() {
429    let mut path = PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::from_all(ABI::V1));
430    let path_ref = &mut path;
431
432    let level = path_ref.as_option_compat_level_mut();
433    assert_eq!(level, &None);
434    assert_eq!(
435        <Option<CompatLevel> as Into<CompatLevel>>::into(*level),
436        CompatLevel::BestEffort
437    );
438
439    path_ref.set_compatibility(CompatLevel::SoftRequirement);
440    assert_eq!(
441        path_ref.as_option_compat_level_mut(),
442        &Some(CompatLevel::SoftRequirement)
443    );
444
445    path.set_compatibility(CompatLevel::HardRequirement);
446}
447
448// It is useful for documentation generation to explicitely implement Rule for every types, instead
449// of doing it generically.
450impl<F> Rule<AccessFs> for PathBeneath<F> where F: AsFd {}
451
452impl<F> PrivateRule<AccessFs> for PathBeneath<F>
453where
454    F: AsFd,
455{
456    const TYPE_ID: uapi::landlock_rule_type = uapi::landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH;
457
458    fn as_ptr(&mut self) -> *const libc::c_void {
459        self.attr.parent_fd = self.parent_fd.as_fd().as_raw_fd();
460        self.attr.allowed_access = self.allowed_access.bits();
461        &self.attr as *const _ as _
462    }
463
464    fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError> {
465        // Checks that this rule doesn't contain a superset of the access-rights handled by the
466        // ruleset.  This check is about requested access-rights but not actual access-rights.
467        // Indeed, we want to get a deterministic behavior, i.e. not based on the running kernel
468        // (which is handled by Ruleset and RulesetCreated).
469        if ruleset.requested_handled_fs.contains(self.allowed_access) {
470            Ok(())
471        } else {
472            Err(AddRuleError::UnhandledAccess {
473                access: self.allowed_access,
474                incompatible: self.allowed_access & !ruleset.requested_handled_fs,
475            }
476            .into())
477        }
478    }
479}
480
481#[test]
482fn path_beneath_check_consistency() {
483    use crate::*;
484
485    let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
486    let rx_access = AccessFs::Execute | AccessFs::ReadFile;
487    assert!(matches!(
488        Ruleset::from(ABI::Unsupported)
489            .handle_access(ro_access)
490            .unwrap()
491            .create()
492            .unwrap()
493            .add_rule(PathBeneath::new(PathFd::new("/").unwrap(), rx_access))
494            .unwrap_err(),
495        RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { access, incompatible }))
496            if access == rx_access && incompatible == AccessFs::Execute
497    ));
498}
499
500/// Simple helper to open a file or a directory with the `O_PATH` flag.
501///
502/// This is the recommended way to identify a path
503/// and manage the lifetime of the underlying opened file descriptor.
504/// Indeed, using other [`AsFd`] implementations such as [`File`] brings more complexity
505/// and may lead to unexpected errors (e.g., denied access).
506///
507/// [`File`]: std::fs::File
508///
509/// # Example
510///
511/// ```
512/// use landlock::{AccessFs, PathBeneath, PathFd, PathFdError};
513///
514/// fn allowed_root_dir(access: AccessFs) -> Result<PathBeneath<PathFd>, PathFdError> {
515///     let fd = PathFd::new("/")?;
516///     Ok(PathBeneath::new(fd, access))
517/// }
518/// ```
519#[derive(Debug)]
520pub struct PathFd {
521    fd: OwnedFd,
522}
523
524impl PathFd {
525    pub fn new<T>(path: T) -> Result<Self, PathFdError>
526    where
527        T: AsRef<Path>,
528    {
529        Ok(PathFd {
530            fd: OpenOptions::new()
531                .read(true)
532                // If the O_PATH is not supported, it is automatically ignored (Linux < 2.6.39).
533                .custom_flags(libc::O_PATH | libc::O_CLOEXEC)
534                .open(path.as_ref())
535                .map_err(|e| PathFdError::OpenCall {
536                    source: e,
537                    path: path.as_ref().into(),
538                })?
539                .into(),
540        })
541    }
542}
543
544impl AsFd for PathFd {
545    fn as_fd(&self) -> BorrowedFd<'_> {
546        self.fd.as_fd()
547    }
548}
549
550#[test]
551fn path_fd() {
552    use std::fs::File;
553    use std::io::Read;
554
555    PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Execute);
556    PathBeneath::new(File::open("/").unwrap(), AccessFs::Execute);
557
558    let mut buffer = [0; 1];
559    // Checks that PathFd really returns an FD opened with O_PATH (Bad file descriptor error).
560    File::from(PathFd::new("/etc/passwd").unwrap().fd)
561        .read(&mut buffer)
562        .unwrap_err();
563}
564
565/// Helper to quickly create an iterator of PathBeneath rules.
566///
567/// # Note
568///
569/// From the kernel's perspective, Landlock rules operate on file descriptors, not paths.
570/// This is a helper to create rules based on paths. Here, `path_beneath_rules()` silently ignores
571/// paths that cannot be opened, hence making the obtainment of a file descriptor impossible. When
572/// possible and for a given path, `path_beneath_rules()` automatically adjusts [access rights](`AccessFs`),
573/// depending on whether a directory or a file is present at that said path.
574///
575/// This behavior is the result of [`CompatLevel::BestEffort`], which is the default compatibility level of
576/// all created rulesets. Thus, it applies to the example below. However, if [`CompatLevel::HardRequirement`]
577/// is set using [`Compatible::set_compatibility`], attempting to create an incompatible rule at runtime will cause
578/// this crate to raise an error instead.
579///
580/// # Example
581///
582/// ```
583/// use landlock::{
584///     ABI, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetStatus, RulesetError,
585///     path_beneath_rules,
586/// };
587///
588/// fn restrict_thread() -> Result<(), RulesetError> {
589///     let abi = ABI::V1;
590///     let status = Ruleset::default()
591///         .handle_access(AccessFs::from_all(abi))?
592///         .create()?
593///         // Read-only access to /usr, /etc and /dev.
594///         .add_rules(path_beneath_rules(&["/usr", "/etc", "/dev"], AccessFs::from_read(abi)))?
595///         // Read-write access to /home and /tmp.
596///         .add_rules(path_beneath_rules(&["/home", "/tmp"], AccessFs::from_all(abi)))?
597///         .restrict_self()?;
598///     match status.ruleset {
599///         // The FullyEnforced case must be tested by the developer.
600///         RulesetStatus::FullyEnforced => println!("Fully sandboxed."),
601///         RulesetStatus::PartiallyEnforced => println!("Partially sandboxed."),
602///         // Users should be warned that they are not protected.
603///         RulesetStatus::NotEnforced => println!("Not sandboxed! Please update your kernel."),
604///     }
605///     Ok(())
606/// }
607/// ```
608pub fn path_beneath_rules<I, P, A>(
609    paths: I,
610    access: A,
611) -> impl Iterator<Item = Result<PathBeneath<PathFd>, RulesetError>>
612where
613    I: IntoIterator<Item = P>,
614    P: AsRef<Path>,
615    A: Into<BitFlags<AccessFs>>,
616{
617    let access = access.into();
618    paths.into_iter().filter_map(move |p| match PathFd::new(p) {
619        Ok(f) => {
620            let valid_access = match is_file(&f) {
621                Ok(true) => access & ACCESS_FILE,
622                // If the stat call failed, let's blindly rely on the requested access rights.
623                Err(_) | Ok(false) => access,
624            };
625            Some(Ok(PathBeneath::new(f, valid_access)))
626        }
627        Err(_) => None,
628    })
629}
630
631#[test]
632fn path_beneath_rules_iter() {
633    let _ = Ruleset::default()
634        .handle_access(AccessFs::from_all(ABI::V1))
635        .unwrap()
636        .create()
637        .unwrap()
638        .add_rules(path_beneath_rules(
639            &["/usr", "/opt", "/does-not-exist", "/root"],
640            AccessFs::Execute,
641        ))
642        .unwrap();
643}