whyno-core 0.4.0

Permission check pipeline, fix engine, and state types
Documentation

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:

  1. Mount — read-only, noexec, nosuid filesystem options
  2. FsFlags — inode flags (immutable, append-only)
  3. Traversal+x on every ancestor directory
  4. DAC — discretionary access control (uid/gid/mode bits + capability overrides)
  5. ACL — POSIX.1e access control lists with mask evaluation
  6. Metadata — ownership and capability checks for chmod, chown, setxattr
  7. SELinux — mandatory access control (feature-gated)
  8. 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 operationschmod, chown (UID/GID), setxattr with 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, and chattr suggestions
  • 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 Serialize for JSON output
  • JSON Schema — all public types derive JsonSchema via schemars, so consumers can generate OpenAPI-compatible schemas from Rust types

Installation

Add whyno-core to your project:

cargo add whyno-core

Or add it manually to your Cargo.toml:

[dependencies]
whyno-core = "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 whyno_core::state::{
    SystemState, Probe,
    subject::ResolvedSubject,
    path::{PathComponent, StatResult, FileType},
    mount::{MountTable, MountEntry, MountOptions},
    mac::MacState,
};
use whyno_core::operation::Operation;

// 1. Identify who is performing the operation
let subject = ResolvedSubject {
    uid: 1000,
    gid: 1000,
    groups: vec![33, 44],             // supplementary groups
    capabilities: Probe::Unknown,      // no process context available
};

// 2. Walk the path from / to the target, gathering stat + ACL + flags
let walk = vec![
    PathComponent {
        path: "/".into(),
        stat: Probe::Known(StatResult {
            mode: 0o755, uid: 0, gid: 0,
            dev: 1, nlink: 2, file_type: FileType::Directory,
        }),
        acl: Probe::Unknown,
        flags: Probe::Unknown,
        mount: Some(0),
    },
    PathComponent {
        path: "/var/log/app.log".into(),
        stat: Probe::Known(StatResult {
            mode: 0o640, uid: 0, gid: 44,
            dev: 1, nlink: 1, file_type: FileType::Regular,
        }),
        acl: Probe::Unknown,
        flags: Probe::Unknown,
        mount: Some(0),
    },
];

// 3. Mount table from /proc/self/mountinfo + statvfs()
let mounts = MountTable(vec![MountEntry {
    mount_id: 1,
    device: 1,
    mountpoint: "/".into(),
    fs_type: "ext4".into(),
    options: MountOptions {
        read_only: false,
        noexec: false,
        nosuid: false,
    },
}]);

// 4. Assemble the full state
let state = SystemState {
    subject,
    walk,
    mounts,
    operation: Operation::Read,
    mac_state: MacState::default(), // SELinux + AppArmor unknown
};

Run Checks

Pass the assembled state into the check pipeline:

use whyno_core::checks::{run_checks, MetadataParams};

// For non-metadata operations, use MetadataParams::default()
let params = MetadataParams::default();
let report = run_checks(&state, &params);

if report.is_allowed() {
    println!("Access allowed");
} else {
    for layer in report.failed_layers() {
        println!("Blocked by: {layer:?}");
    }
}

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 whyno_core::fix::generate_fixes;

let plan = generate_fixes(&report, &state, &params);

for fix in &plan.fixes {
    println!(
        "[impact {}] {:?}{}",
        fix.impact, fix.action, fix.description
    );
}

for warning in &plan.warnings {
    println!("{warning}");
}

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 whyno_core::operation::{Operation, MetadataParams, XattrNamespace};
use whyno_core::checks::run_checks;

// Check whether the subject can chmod the target file
let state = SystemState {
    operation: Operation::Chmod,
    ..state // reuse subject, walk, mounts, mac_state from above
};
let params = MetadataParams::default();
let report = run_checks(&state, &params);

// For chown with a specific target GID, populate MetadataParams
let state = SystemState {
    operation: Operation::ChownGid,
    ..state
};
let params = MetadataParams {
    new_gid: Some(33), // target group
    ..MetadataParams::default()
};
let report = run_checks(&state, &params);

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 GIDCAP_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:

cargo add whyno-core --features selinux,apparmor

Dependencies

whyno-core is intentionally lightweight — no system dependencies, no I/O crates:

  • serde + serde_json — serialization for all public types
  • schemarsJsonSchema derive for schema generation
  • thiserror — structured error types
  • enum-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.