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_access xattr bytes
  • Filesystem flags — immutable and append-only flags via ioctl(FS_IOC_GETFLAGS)
  • Mount info — filesystem type, device, mountpoint from /proc/self/mountinfo
  • Mount optionsread_only, noexec, nosuid from statvfs() (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 complete SystemState
  • Ancestor walk — every directory from / to the target is stat'd, ACL'd, and flag-checked
  • Authoritative mount flagsstatvfs() is the source of truth for ro/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(…) or Probe::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, and SetXattr operations with MetadataParams for caller intent (new mode, new uid/gid, xattr namespace)

Installation

Add the crate to your project:

cargo add whyno-gather

Or 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 selinux

Quick 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 /proc

Modules

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 absolute
  • MountInfoUnreadable — could not read /proc/self/mountinfo
  • StatFailedstat() call failed for a path
  • UserNotFound / UidNotFound — subject resolution failed
  • ProcUnreadable — could not read /proc/<pid>/status
  • ServiceResolveFailed — systemd service PID lookup failed
  • ParseError — malformed /proc or passwd content
  • Other — 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-core 0.4 — shared types (SystemState, Operation, MetadataParams, Probe, etc.)
  • nix — safe Rust bindings for stat(), ioctl, user/group resolution
  • libc — raw syscall constants and types
  • thiserror — structured error types
  • selinux (optional) — SELinux context API (only with selinux feature)

Requirements

  • Linux only — relies on /proc, xattrs, ioctl, and statvfs
  • Rust 1.78+ (workspace MSRV)
  • Runtime dependency on whyno-core for shared types

Integration Pattern

The typical integration pattern is:

  1. Use whyno-gather to collect OS state into a SystemState
  2. Pass that SystemState to whyno-core's run_checks() for evaluation
  3. If checks fail, pass the report to whyno-core's generate_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, &params);

if !report.is_allowed() {
    let plan = generate_fixes(&report, &state, &params);
    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, &params);
// Metadata layer checks ownership + CAP_FOWNER for chmod