switchyard/fs/
mount.rs

1//! Filesystem mount inspection and policy helpers.
2
3use crate::types::{MountError, MountFlags};
4use std::path::{Path, PathBuf};
5
6pub trait MountInspector {
7    /// Get mount flags for a path.
8    ///
9    /// # Errors
10    ///
11    /// Returns a `MountError` if mount information cannot be determined.
12    fn flags_for(&self, path: &Path) -> Result<MountFlags, MountError>;
13}
14
15/// Production inspector. Prefer kernel syscalls when available; fall back to parsing /proc/self/mounts.
16#[derive(Debug, Copy, Clone)]
17pub struct ProcStatfsInspector;
18
19impl ProcStatfsInspector {
20    fn flags_via_statvfs(path: &Path) -> Result<MountFlags, MountError> {
21        // Use rustix::fs::statvfs to query mount flags when available
22        match rustix::fs::statvfs(path) {
23            Ok(vfs) => {
24                let flags = vfs.f_flag;
25                let read_only = flags.contains(rustix::fs::StatVfsMountFlags::RDONLY);
26                let no_exec = flags.contains(rustix::fs::StatVfsMountFlags::NOEXEC);
27                Ok(MountFlags { read_only, no_exec })
28            }
29            Err(_) => Err(MountError::Unknown),
30        }
31    }
32    fn parse_proc_mounts(path: &Path) -> Result<MountFlags, MountError> {
33        // Canonicalize best-effort; if it fails, still proceed with the raw path
34        let p = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
35        let content =
36            std::fs::read_to_string("/proc/self/mounts").map_err(|_| MountError::Unknown)?;
37        let mut best: Option<(PathBuf, String)> = None;
38        for line in content.lines() {
39            let parts: Vec<&str> = line.split_whitespace().collect();
40            if parts.len() < 4 {
41                continue;
42            }
43            let mnt = parts.get(1).map(PathBuf::from).ok_or(MountError::Unknown)?;
44            if p.starts_with(&mnt) {
45                let opts = parts
46                    .get(3)
47                    .ok_or(MountError::Unknown)?
48                    .to_ascii_lowercase();
49                match &best {
50                    None => best = Some((mnt, opts)),
51                    Some((b, _)) => {
52                        if mnt.as_os_str().len() > b.as_os_str().len() {
53                            best = Some((mnt, opts));
54                        }
55                    }
56                }
57            }
58        }
59        if let Some((_mnt, opts)) = best {
60            let has_rw = opts.split(',').any(|o| o == "rw");
61            let noexec = opts.split(',').any(|o| o == "noexec");
62            Ok(MountFlags {
63                read_only: !has_rw,
64                no_exec: noexec,
65            })
66        } else {
67            Err(MountError::Unknown)
68        }
69    }
70}
71
72impl MountInspector for ProcStatfsInspector {
73    fn flags_for(&self, path: &Path) -> Result<MountFlags, MountError> {
74        // Prefer kernel statvfs; fall back to /proc parsing on error
75        Self::flags_via_statvfs(path).or_else(|_| Self::parse_proc_mounts(path))
76    }
77}
78
79/// Policy helper: Ensure that the filesystem containing `path` is mounted with both read and write permissions.
80///
81/// # Errors
82///
83/// Returns a `MountError` if the filesystem is not mounted with read and write permissions.
84pub fn ensure_rw_exec(inspector: &impl MountInspector, path: &Path) -> Result<(), MountError> {
85    match inspector.flags_for(path) {
86        Ok(flags) => {
87            if flags.read_only || flags.no_exec {
88                return Err(MountError::Unknown);
89            }
90            Ok(())
91        }
92        Err(e) => Err(e),
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    struct MockInspector {
101        flags: Result<MountFlags, MountError>,
102    }
103    impl MountInspector for MockInspector {
104        fn flags_for(&self, _path: &Path) -> Result<MountFlags, MountError> {
105            self.flags
106        }
107    }
108
109    #[test]
110    fn ensure_rw_exec_passes_on_rw_exec() {
111        let ins = MockInspector {
112            flags: Ok(MountFlags {
113                read_only: false,
114                no_exec: false,
115            }),
116        };
117        assert!(ensure_rw_exec(&ins, Path::new("/tmp")).is_ok());
118    }
119
120    #[test]
121    fn ensure_rw_exec_fails_on_ro_or_noexec() {
122        let ins1 = MockInspector {
123            flags: Ok(MountFlags {
124                read_only: true,
125                no_exec: false,
126            }),
127        };
128        assert!(ensure_rw_exec(&ins1, Path::new("/tmp")).is_err());
129        let ins2 = MockInspector {
130            flags: Ok(MountFlags {
131                read_only: false,
132                no_exec: true,
133            }),
134        };
135        assert!(ensure_rw_exec(&ins2, Path::new("/tmp")).is_err());
136    }
137
138    #[test]
139    fn ensure_rw_exec_fails_on_ambiguous() {
140        let ins = MockInspector {
141            flags: Err(MountError::Unknown),
142        };
143        assert!(ensure_rw_exec(&ins, Path::new("/tmp")).is_err());
144    }
145}