**Permission check pipeline, fix engine, and state types for Linux filesystem permissions.**
`whyno-core` is the pure-logic engine behind the [whyno](https://gnu.foo/projects/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 operations** — `chmod`, `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:
```bash
cargo add whyno-core
```
Or add it manually to your `Cargo.toml`:
```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.
```rust
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:
```rust
use whyno_core::checks::{run_checks, MetadataParams};
// For non-metadata operations, use MetadataParams::default()
let params = MetadataParams::default();
let report = run_checks(&state, ¶ms);
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:
```rust
use whyno_core::fix::generate_fixes;
let plan = generate_fixes(&report, &state, ¶ms);
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`:
```rust
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, ¶ms);
// 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, ¶ms);
```
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
| `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
| `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
| `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`.
| 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
| `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:
```bash
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
- `schemars` — `JsonSchema` 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.