Permission check pipeline, fix engine, and state types for Linux filesystem permissions.
whyno-core is the pure-logic engine behind the whyno permission debugger. It takes a pre-gathered snapshot of filesystem state and deterministically evaluates whether an operation is allowed — with zero I/O at check time. When a check fails, it generates least-privilege fix suggestions ranked by impact.
Overview
whyno-core answers one question: "Why can't this user do this to that file?"
Given a SystemState (subject identity, path walk, mount table, MAC state, and the requested operation), the check pipeline evaluates eight layers of Linux permission enforcement:
- Mount — read-only,
noexec,nosuidfilesystem options - FsFlags — inode flags (
immutable,append-only) - Traversal —
+xon every ancestor directory - DAC — discretionary access control (uid/gid/mode bits + capability overrides)
- ACL — POSIX.1e access control lists with mask evaluation
- Metadata — ownership and capability checks for
chmod,chown,setxattr - SELinux — mandatory access control (feature-gated)
- AppArmor — mandatory access control (feature-gated)
Each layer returns Pass, Fail, or Degraded (state unavailable). The fix engine then generates repair suggestions for every failed layer, simulates their cascading effects, and prunes redundant fixes.
Features
- Pure logic, zero I/O — all filesystem state is gathered externally and passed in via
SystemState - Eight-layer check pipeline — mount options, fs flags, traversal, DAC, ACL, metadata, SELinux, AppArmor
- Metadata operations —
chmod,chown(UID/GID),setxattrwith per-namespace capability rules - Capability-aware DAC — models
CAP_DAC_OVERRIDE,CAP_DAC_READ_SEARCH,CAP_FOWNER, and more - POSIX.1e ACL evaluation — full mask interaction, named user/group entries, effective permission computation
- Fix engine — generates
chmod,chown,setfacl,mount -o remount, andchattrsuggestions - Impact scoring — fixes ranked 1 (least privilege) to 6 (broadest impact)
- Cascade simulation — applies fixes to cloned state, prunes cross-layer redundancies
- Serializable — all types derive
Serializefor JSON output - JSON Schema — all public types derive
JsonSchemaviaschemars, so consumers can generate OpenAPI-compatible schemas from Rust types
Installation
Add whyno-core to your project:
Or add it manually to your Cargo.toml:
[]
= "0.4"
Quick Start
Build a SystemState
The check pipeline operates on a SystemState struct that you build from gathered OS data. This is the only input — the pipeline performs no I/O.
use ;
use Operation;
// 1. Identify who is performing the operation
let subject = ResolvedSubject ;
// 2. Walk the path from / to the target, gathering stat + ACL + flags
let walk = vec!;
// 3. Mount table from /proc/self/mountinfo + statvfs()
let mounts = MountTable;
// 4. Assemble the full state
let state = SystemState ;
Run Checks
Pass the assembled state into the check pipeline:
use ;
// For non-metadata operations, use MetadataParams::default()
let params = default;
let report = run_checks;
if report.is_allowed else
The CheckReport contains per-layer results (Pass, Fail, or Degraded) for all eight layers. Use is_allowed() for a quick verdict or inspect individual layers for detailed diagnostics.
Generate Fixes
When checks fail, generate fix suggestions ranked by impact:
use generate_fixes;
let plan = generate_fixes;
for fix in &plan.fixes
for warning in &plan.warnings
The fix engine produces structured FixAction variants (Chmod, Chown, SetAcl, Remount, Chattr, GrantCap) that can be rendered to shell commands at output time. The cascade simulator prunes fixes that become redundant after an earlier fix resolves a later layer's failure.
Metadata Operations
For metadata-change operations (chmod, chown, setxattr), use the dedicated Operation variants and supply a MetadataParams:
use ;
use run_checks;
// Check whether the subject can chmod the target file
let state = SystemState ;
let params = default;
let report = run_checks;
// For chown with a specific target GID, populate MetadataParams
let state = SystemState ;
let params = MetadataParams ;
let report = run_checks;
Metadata operations bypass the DAC and ACL layers (which return Pass immediately) and are evaluated by the dedicated Metadata layer using ownership and capability rules.
Key Types
State Types
| Type | Module | Purpose |
|---|---|---|
SystemState |
state |
Complete gathered state for a permission query — subject, path walk, mounts, operation, MAC state |
Probe<T> |
state |
Tri-state wrapper: Known(T), Unknown, or Inaccessible — never produces false-green results |
ResolvedSubject |
state::subject |
UID, GID, supplementary groups, and effective capability bitmask |
PathComponent |
state::path |
Single path segment with stat, ACL, fs-flags, and mount reference |
MountTable |
state::mount |
Collection of mount entries with device-based lookup |
PosixAcl |
state::acl |
POSIX.1e ACL with mask application and effective permission helpers |
MacState |
state::mac |
SELinux and AppArmor probe results |
Operation |
operation |
Read, Write, Execute, Delete, Create, Stat, Chmod, ChownUid, ChownGid, or SetXattr — determines which checks to run |
MetadataParams |
operation |
Caller-supplied intent for metadata ops — target mode, UID, GID. Use Default::default() for non-metadata operations |
XattrNamespace |
operation |
User, Trusted, Security, or SystemPosixAcl — determines capability requirements for SetXattr |
Check Types
| Type | Purpose |
|---|---|
CheckReport |
Aggregated results from all check layers — is_allowed() for quick verdict, failed_layers() for diagnostics |
LayerResult |
Pass (with optional warnings), Fail (with detail and component index), or Degraded (state unavailable) |
CoreLayer |
Enum identifying core layers: Mount, FsFlags, Traversal, Dac, Acl, Metadata |
MacLayer |
Enum identifying MAC layers: SeLinux, AppArmor |
Fix Types
| Type | Purpose |
|---|---|
FixPlan |
Ordered fix list with warnings for high-impact changes |
Fix |
Single suggestion: target layer, structured action, impact score (1–6), and description |
FixAction |
Chmod, Chown, SetAcl, Remount, Chattr, or GrantCap — renderable to shell commands |
Check Pipeline
The run_checks() function evaluates layers in a fixed order. Each layer is a pure function that takes &SystemState (and &MetadataParams for the metadata layer) and returns a LayerResult.
| Order | Layer | What it checks | Blocks on |
|---|---|---|---|
| 1 | Mount | Filesystem mount options | ro for writes, noexec for execute, nosuid advisory |
| 2 | FsFlags | Inode flags via FS_IOC_GETFLAGS |
immutable blocks all writes, append-only blocks non-append writes |
| 3 | Traversal | +x on every ancestor directory |
Missing execute on any component in the path walk |
| 4 | DAC | uid/gid/mode bits + capabilities | Missing permission bits (after CAP_DAC_OVERRIDE etc.) |
| 5 | ACL | POSIX.1e access control list | ACL denies access after mask application |
| 6 | Metadata | Ownership + capability rules | chmod without ownership/CAP_FOWNER, chown without CAP_CHOWN, setxattr without required cap |
| 7 | SELinux | Mandatory access control | AVC denial (requires selinux feature) |
| 8 | AppArmor | Mandatory access control | Profile denial (requires apparmor feature) |
The DAC layer automatically applies capability overrides. If the subject has CAP_DAC_OVERRIDE (or uid == 0 when capabilities are unknown), read/write checks on regular files pass regardless of mode bits. CAP_DAC_READ_SEARCH grants read access to files and search access to directories.
Metadata operations (Chmod, ChownUid, ChownGid, SetXattr) bypass the DAC and ACL layers entirely — they are governed by ownership and capability rules in the dedicated Metadata layer. The metadata check implements kernel setattr_prepare semantics:
- chmod — file owner or
CAP_FOWNER - chown UID — requires
CAP_CHOWN - chown GID —
CAP_CHOWN, or file owner who is a member of the target group - setxattr user/posix_acl — file owner or
CAP_FOWNER - setxattr trusted/security — requires
CAP_SYS_ADMIN
Feature Flags
| Feature | Default | Effect |
|---|---|---|
selinux |
off | Enables SELinux check layer — without it, SELinux results are always Degraded |
apparmor |
off | Enables AppArmor check layer — without it, AppArmor results are always Degraded |
test-helpers |
off | Exposes test_helpers module with builders for constructing SystemState in tests |
Enable MAC layers when building for systems that use them:
Dependencies
whyno-core is intentionally lightweight — no system dependencies, no I/O crates:
serde+serde_json— serialization for all public typesschemars—JsonSchemaderive for schema generationthiserror— structured error typesenum-map— fixed-size enum-keyed maps (used for layer results)
Minimum Supported Rust Version
The MSRV is Rust 1.78 (enforced in CI). whyno-core is a pure Rust crate with no platform-specific code — it compiles on any target rustc supports.