Skip to main content

whyno_gather/
lib.rs

1//! Os-level state gathering for permission debugging.
2//!
3//! Handles all system interaction: `stat()`, `getxattr()`,
4//! `ioctl(FS_IOC_GETFLAGS)`, `statvfs()`, `/proc` parsing, and
5//! `/etc/passwd` + `/etc/group` resolution. gathered data is packaged
6//! Into [`whyno_core::state::SystemState`] for the check pipeline.
7//!
8//! Mount flags (`ro`, `noexec`, `nosuid`) come from `statvfs()` — one
9//! Syscall, no text parsing. `/proc/self/mountinfo` is retained for
10//! Metadata only (`fs_type`, `mountpoint`, `device`).
11
12#![deny(clippy::all)]
13#![warn(clippy::pedantic)]
14#![deny(missing_docs)]
15
16pub mod acl;
17pub mod error;
18pub mod fsflags;
19pub mod mac;
20pub mod mountinfo;
21pub mod proc;
22pub mod stat;
23pub mod statvfs;
24pub mod subject;
25
26use std::path::{Path, PathBuf};
27
28use whyno_core::operation::Operation;
29use whyno_core::state::path::PathComponent;
30use whyno_core::state::subject::ResolvedSubject;
31use whyno_core::state::SystemState;
32
33use error::GatherError;
34
35/// Gathers complete OS state for a permission query.
36///
37/// Subject must already be resolved. this function:
38///
39/// 1. Validates path is absolute.
40/// 2. Splits path into ancestor components.
41/// 3. Stats each component, gathers ACLs and filesystem flags.
42/// 4. Parses `/proc/self/mountinfo` for metadata (`fs_type`, device, mountpoint).
43/// 5. Overlays `statvfs()` flags onto mount entries (authoritative for ro/noexec/nosuid).
44/// 6. Links mounts to components by longest-prefix + device match.
45/// 7. Assembles final [`SystemState`].
46///
47/// # Errors
48///
49/// Returns `GatherError::RelativePath` if path is not absolute,
50/// Or `GatherError::MountInfoUnreadable` if `/proc/self/mountinfo` fails.
51pub fn gather_state(
52    subject: &ResolvedSubject,
53    operation: Operation,
54    path: &Path,
55) -> Result<SystemState, GatherError> {
56    if !path.is_absolute() {
57        return Err(GatherError::RelativePath(path.to_owned()));
58    }
59
60    let ancestors = split_ancestors(path);
61    let components: Vec<PathComponent> = ancestors.iter().map(|p| gather_component(p)).collect();
62    let mut mounts = mountinfo::parse_mountinfo()?;
63    overlay_statvfs_flags(&mut mounts);
64    let walk = link_mounts_to_components(components, &mounts);
65
66    Ok(SystemState {
67        subject: subject.clone(),
68        walk,
69        mounts,
70        operation,
71        mac_state: mac::gather_mac_state(operation, path),
72    })
73}
74
75/// Overwrites mount option flags with `statvfs()` results.
76///
77/// `statvfs()` is the authoritative source for `read_only`, `noexec`,
78/// `nosuid` — one syscall, no text parsing. mountinfo options are
79/// Retained only as fallback when `statvfs()` fails on a mountpoint.
80fn overlay_statvfs_flags(mounts: &mut whyno_core::state::mount::MountTable) {
81    for entry in &mut mounts.0 {
82        if let whyno_core::state::Probe::Known(opts) =
83            statvfs::probe_mount_options(&entry.mountpoint)
84        {
85            entry.options = opts;
86        }
87    }
88}
89
90/// Splits absolute path into ancestor chain from `/` to target.
91///
92/// Example: `/var/log/app.log` -> `["/", "/var", "/var/log", "/var/log/app.log"]`
93fn split_ancestors(path: &Path) -> Vec<PathBuf> {
94    let mut ancestors: Vec<PathBuf> = path.ancestors().map(Path::to_path_buf).collect();
95    ancestors.reverse();
96    ancestors
97}
98
99/// Gathers stat, ACL, and filesystem flags for a single path component.
100fn gather_component(path: &Path) -> PathComponent {
101    PathComponent {
102        path: path.to_owned(),
103        stat: stat::stat_component(path),
104        acl: acl::get_acl(path),
105        flags: fsflags::get_fs_flags(path),
106        mount: None,
107    }
108}
109
110/// Links each component to its best-matching mount entry.
111///
112/// For each component with a known `st_dev`, finds mount entry whose
113/// Mountpoint is longest prefix of component path and whose device
114/// Matches. stores mount table index in `component.mount`.
115fn link_mounts_to_components(
116    mut components: Vec<PathComponent>,
117    mounts: &whyno_core::state::mount::MountTable,
118) -> Vec<PathComponent> {
119    for comp in &mut components {
120        comp.mount = find_best_mount(comp, mounts);
121    }
122    components
123}
124
125/// Finds mount table index for a component by longest-prefix + device match.
126fn find_best_mount(
127    comp: &PathComponent,
128    mounts: &whyno_core::state::mount::MountTable,
129) -> Option<usize> {
130    let dev = comp.stat.known().map(|s| s.dev)?;
131    mounts
132        .0
133        .iter()
134        .enumerate()
135        .filter(|(_, m)| m.device == dev && comp.path.starts_with(&m.mountpoint))
136        .max_by_key(|(_, m)| m.mountpoint.as_os_str().len())
137        .map(|(idx, _)| idx)
138}
139
140#[cfg(test)]
141#[path = "lib_tests.rs"]
142mod tests;