whyno-gather
whyno-gather is the OS-level state-gathering crate in the whyno project. It collects Linux file-permission metadata — stat(), POSIX ACLs, filesystem flags, mount options, MAC labels (SELinux/AppArmor), and process capabilities — and packages everything into a single SystemState struct for the check pipeline.
What It Does
Given an absolute file path, a resolved subject (user/group), and an operation, whyno-gather walks the entire ancestor chain from / to the target and collects:
- File stat — ownership (uid/gid), mode bits, file type, device ID
- POSIX ACLs — parsed directly from
system.posix_acl_accessxattr bytes - Filesystem flags — immutable and append-only flags via
ioctl(FS_IOC_GETFLAGS) - Mount info — filesystem type, device, mountpoint from
/proc/self/mountinfo - Mount options —
read_only,noexec,nosuidfromstatvfs()(authoritative, no text parsing) - MAC labels — SELinux file/process contexts and AppArmor profiles
- Process info — effective capabilities, UID/GID from
/proc/<pid>/status
Features
- Single entry point — call
gather_state()and get a completeSystemState - Ancestor walk — every directory from
/to the target is stat'd, ACL'd, and flag-checked - Authoritative mount flags —
statvfs()is the source of truth forro/noexec/nosuid; mountinfo text options are fallback only - Subject resolution — resolve by username, UID, PID, or systemd service name
- Graceful degradation — each probe returns
Probe::Known(…)orProbe::Unknown(reason)instead of failing the entire gather - Feature-gated MAC — SELinux and AppArmor support are opt-in via Cargo features
- Metadata operations (v0.4) — supports
Chmod,ChownUid,ChownGid, andSetXattroperations withMetadataParamsfor caller intent (new mode, new uid/gid, xattr namespace)
Installation
Add the crate to your project:
cargo add whyno-gatherOr add it manually to your Cargo.toml:
[dependencies]
whyno-gather = "0.4"Feature Flags
| Flag | What it enables | Default |
|---|---|---|
selinux |
SELinux context gathering via the selinux crate |
off |
apparmor |
AppArmor profile detection | off |
ext4-tests |
Run integration tests that require an ext4 filesystem | off |
acl-tests |
Run integration tests that require POSIX ACL support | off |
Enable SELinux support:
cargo add whyno-gather --features selinuxQuick Start
Gather full state for a file
use whyno_gather::gather_state;
use whyno_gather::subject::resolve_username;
// Resolve the subject (user running the operation)
let subject = resolve_username("deploy")?;
// Gather complete OS state for the target path
let state = gather_state(
&subject,
whyno_core::operation::Operation::Read,
std::path::Path::new("/var/log/app.log"),
)?;
// state.walk contains stat, ACL, and flags for each ancestor
// state.mounts contains the mount table with statvfs-verified options
// state.mac_state contains SELinux/AppArmor labels (if features enabled)Resolve a subject by UID
use whyno_gather::subject::resolve_uid;
let subject = resolve_uid(33)?; // www-data
println!("uid={} gid={} groups={:?}", subject.uid, subject.gid, subject.groups);Resolve a subject by PID
use whyno_gather::subject::resolve_pid;
let subject = resolve_pid(1234)?;
// Reads /proc/<pid>/status for UID, GID, supplementary groups, and capabilities
println!("uid={} gid={}", subject.uid, subject.gid);Resolve a systemd service
use whyno_gather::subject::resolve_service;
let subject = resolve_service("nginx.service")?;
// Looks up MainPID via systemctl, then resolves via /procModules
| Module | Purpose |
|---|---|
acl |
POSIX ACL reading — parses raw xattr bytes into typed ACL entries |
stat |
File stat gathering — ownership, mode, file type classification |
statvfs |
Filesystem stat — probes mount options via statvfs() syscall |
mountinfo |
Mount info parsing from /proc/self/mountinfo |
proc |
Process info — reads /proc/<pid>/status for UIDs, GIDs, capabilities |
subject |
Subject resolution — username, UID, PID, or service → ResolvedSubject |
fsflags |
Filesystem flags — reads immutable/append-only via ioctl |
mac |
MAC label gathering — SELinux contexts and AppArmor profiles |
error |
Error types — GatherError enum covering all failure modes |
Error Handling
All fallible operations return Result<…, GatherError>. The main error variants:
RelativePath— path must be absoluteMountInfoUnreadable— could not read/proc/self/mountinfoStatFailed—stat()call failed for a pathUserNotFound/UidNotFound— subject resolution failedProcUnreadable— could not read/proc/<pid>/statusServiceResolveFailed— systemd service PID lookup failedParseError— malformed/procor passwd contentOther— catch-all for unexpected failures
Individual per-component probes (ACLs, flags, MAC labels) use Probe::Unknown rather than hard errors, so a single inaccessible attribute does not abort the entire gather.
Dependencies
whyno-core0.4— shared types (SystemState,Operation,MetadataParams,Probe, etc.)nix— safe Rust bindings forstat(),ioctl, user/group resolutionlibc— raw syscall constants and typesthiserror— structured error typesselinux(optional) — SELinux context API (only withselinuxfeature)
Requirements
- Linux only — relies on
/proc, xattrs,ioctl, andstatvfs - Rust 1.78+ (workspace MSRV)
- Runtime dependency on
whyno-corefor shared types
Integration Pattern
The typical integration pattern is:
- Use
whyno-gatherto collect OS state into aSystemState - Pass that
SystemStatetowhyno-core'srun_checks()for evaluation - If checks fail, pass the report to
whyno-core'sgenerate_fixes()for repair suggestions
use whyno_gather::{gather_state, subject::resolve_username};
use whyno_core::checks::{run_checks, MetadataParams};
use whyno_core::fix::generate_fixes;
use whyno_core::operation::Operation;
use std::path::Path;
let subject = resolve_username("deploy")?;
let state = gather_state(&subject, Operation::Write, Path::new("/var/log/app.log"))?;
let params = MetadataParams::default();
let report = run_checks(&state, ¶ms);
if !report.is_allowed() {
let plan = generate_fixes(&report, &state, ¶ms);
for fix in &plan.fixes {
eprintln!("[impact {}] {:?}", fix.impact, fix.action);
}
}Check a metadata operation (v0.4)
use whyno_gather::{gather_state, subject::resolve_username};
use whyno_core::checks::{run_checks, MetadataParams};
use whyno_core::operation::Operation;
use std::path::Path;
let subject = resolve_username("deploy")?;
let state = gather_state(&subject, Operation::Chmod, Path::new("/etc/app.conf"))?;
let params = MetadataParams {
new_mode: Some(0o644),
..Default::default()
};
let report = run_checks(&state, ¶ms);
// Metadata layer checks ownership + CAP_FOWNER for chmod