whyno-core 0.3.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 seven 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. SELinux — mandatory access control (feature-gated)
  7. 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
  • Seven-layer check pipeline — mount options, fs flags, traversal, DAC, ACL, SELinux, AppArmor
  • 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

Installation

[dependencies]
whyno-core = "0.3"

Quick Start

Build a SystemState

The check pipeline operates on a SystemState struct built from gathered OS data. 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;

let subject = ResolvedSubject {
    uid: 1000,
    gid: 1000,
    groups: vec![33, 44],
    capabilities: Probe::Unknown,
};

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),
    },
];

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 },
}]);

let state = SystemState {
    subject,
    walk,
    mounts,
    operation: Operation::Read,
    mac_state: MacState::default(),
};

Run Checks

use whyno_core::checks::run_checks;

let report = run_checks(&state);

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

Generate Fixes

use whyno_core::fix::generate_fixes;

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

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

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

Key Types

State Types

Type Module Purpose
SystemState state Complete gathered state — subject, path walk, mounts, operation, MAC state
Probe<T> state Tri-state: 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, or Stat

Check Types

Type Purpose
CheckReport Aggregated results — is_allowed() for quick verdict, failed_layers() for diagnostics
LayerResult Pass (with optional warnings), Fail (with detail), or Degraded (state unavailable)
CoreLayer Mount, FsFlags, Traversal, Dac, Acl
MacLayer SeLinux, AppArmor

Fix Types

Type Purpose
FixPlan Ordered fix list with warnings for high-impact changes
Fix Target layer, structured action, impact score (1–6), and description
FixAction Chmod, Chown, SetAcl, Remount, or Chattr — renderable to shell commands

Check Pipeline

run_checks() evaluates layers in a fixed order. All layers always run — no short-circuiting.

Order Layer What it checks
1 Mount ro for writes, noexec for execute, nosuid advisory
2 FsFlags immutable blocks all writes, append-only blocks non-append writes
3 Traversal +x on every ancestor directory
4 DAC uid/gid/mode bits + capability overrides
5 ACL POSIX.1e named entries with mask application
6 SELinux AVC decision over pre-gathered mac_state (selinux feature)
7 AppArmor Profile mode over pre-gathered mac_state (apparmor feature)

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 fluent builders for constructing SystemState in tests

Dependencies

  • serde + serde_json — serialization for all public types
  • schemarsJsonSchema derive for schema generation
  • thiserror — structured error types
  • enum-map — fixed-size enum-keyed maps for layer results

Minimum Supported Rust Version

Rust 1.78. Pure Rust, no platform-specific code — compiles on any target rustc supports.