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