whyno-core

Pure-logic library crate. Zero I/O, zero syscalls. All filesystem state is passed in via SystemState.

Dependencies: serde, serde_json, thiserror

Module Map

whyno-core/src/
├── lib.rs              # Crate root, module declarations
├── operation.rs        # Operation, XattrNamespace, MetadataParams
├── state/              # State representation types
│   ├── mod.rs          # SystemState, Probe<T>
│   ├── subject.rs      # ResolvedSubject { uid, gid, groups }
│   ├── path.rs         # PathComponent, StatResult, FileType
│   ├── mount.rs        # MountEntry, MountTable, MountOptions
│   ├── acl.rs          # AclEntry, AclTag, AclPerms, PosixAcl
│   ├── fsflags.rs      # FsFlags { immutable, append_only }
│   └── mac.rs          # MacState, SeLinuxState, SeLinuxMode, AppArmorState
├── checks/             # One module per permission layer
│   ├── mod.rs          # CheckReport, LayerResult, CoreLayer, MacLayer, run_checks()
│   ├── mount.rs        # check_mount() — ro, noexec, nosuid
│   ├── fsflags.rs      # check_fs_flags() — immutable, append-only
│   ├── traversal.rs    # check_traversal() — +x on ancestors
│   ├── dac.rs          # check_dac() — owner/group/other rwx
│   ├── acl.rs          # check_acl() — POSIX ACL evaluation with mask
│   ├── caps.rs         # Capability constants (+ CAP_FSETID, CAP_SYS_ADMIN in v0.4) and has_cap()
│   ├── caps_modify.rs  # capability_modify() — CAP_DAC_OVERRIDE post-DAC modifier
│   ├── metadata.rs     # check_metadata() — chmod/chown/setxattr checks (v0.4)
│   ├── selinux.rs      # check_selinux() — pure function over &SystemState (--features selinux)
│   └── apparmor.rs     # check_apparmor() — pure function over &SystemState (--features apparmor)
├── fix/                # Fix engine
│   ├── mod.rs          # FixPlan, Fix, FixAction, generate_fixes()
│   ├── generators.rs   # Per-layer fix generation functions
│   ├── scoring.rs      # Impact scoring (1–6 scale)
│   ├── cascade.rs      # Cascade simulationsimulate_cascade(), prune redundant fixes
│   ├── cascade/
│   │   └── apply.rs    # apply_fix() — semantic state mutation per FixAction
│   └── commands.rs     # FixActionshell command rendering
└── test_helpers/       # Synthetic state builders (gated behind test-helpers feature)
    ├── mod.rs
    ├── internal.rs
    └── tests.rs

state — State Representation

Probe<T>

Tri-state wrapper for gathered data. Every OS-sourced value is wrapped in Probe to distinguish "successfully read," "couldn't read," and "permission denied during read."

enum Probe<T> {
    Known(T),       // Successfully gathered
    Unknown,        // General failure (syscall error, missing /proc)
    Inaccessible,   // Explicit EACCES/EPERM on the probe
}

Key methods:

  • known() -> Option<&T> — Returns Some only for Known
  • is_known() -> bool
  • map(f) -> Probe<U> — Transforms inner value, preserves Unknown/Inaccessible
  • as_ref() -> Probe<&T>

Check functions treat Unknown and Inaccessible as degraded — never false-green.

SystemState

The complete gathered state for a single permission query.

struct SystemState {
    subject: ResolvedSubject,    // Resolved identity (uid, gid, groups)
    walk: Vec<PathComponent>,    // Path components from / to target
    mounts: MountTable,          // Parsed /proc/self/mountinfo
    operation: Operation,        // The operation being checked
    mac_state: MacState,         // SELinux + AppArmor pre-gathered state (v0.3)
}

ResolvedSubject

Normalized identity — all 4 input formats (username, UID, PID, service) collapse to this before checks run.

struct ResolvedSubject {
    uid: u32,                      // Effective user ID
    gid: u32,                      // Primary group ID
    groups: Vec<u32>,              // Supplementary groups (excludes primary)
    capabilities: Probe<u64>,      // CapEff bitmask; Known for pid:/svc:, Unknown for username/UID
}

Key method: in_group(gid) -> bool — checks both primary and supplementary groups.

PathComponent

A single component in the path walk from / to the target. Each ancestor independently carries its permissions state.

struct PathComponent {
    path: PathBuf,
    stat: Probe<StatResult>,     // mode, uid, gid, dev, nlink, file_type
    acl: Probe<PosixAcl>,        // POSIX ACL entries
    flags: Probe<FsFlags>,       // Filesystem inode flags
    mount: Option<usize>,        // Index into SystemState::mounts
}

StatResult

struct StatResult {
    mode: u32,           // Permission mode bits (e.g., 0o755)
    uid: u32,            // Owner UID
    gid: u32,            // Owner GID
    dev: u64,            // Device ID (st_dev) — joins with mount table
    nlink: u64,          // Hard link count
    file_type: FileType, // Regular, Directory, Symlink, Other
}

MountTable / MountEntry

struct MountTable(pub Vec<MountEntry>);

struct MountEntry {
    mount_id: u32,
    device: u64,               // Matches StatResult::dev
    mountpoint: PathBuf,
    fs_type: String,           // "ext4", "tmpfs", "xfs", etc.
    options: MountOptions,     // { read_only, noexec, nosuid }
}

find_by_device(dev) -> Option<&MountEntry> returns the entry with the longest mountpoint for correct bind-mount resolution.

PosixAcl

Models system.posix_acl_access extended attribute content per POSIX.1e.

struct PosixAcl(pub Vec<AclEntry>);

struct AclEntry {
    tag: AclTag,               // UserObj, User, GroupObj, Group, Mask, Other
    qualifier: Option<u32>,    // UID/GID for named entries
    perms: AclPerms,           // { read, write, execute }
}

Key methods:

  • mask() -> Option<&AclPerms> — The mask limits effective perms for named user, owning group, and named group entries
  • named_user(uid) -> Option<&AclEntry>
  • named_group(gid) -> Option<&AclEntry>
  • has_extended_entries() -> bool — True if named User or Group entries exist (triggers POSIX.1e evaluation)
  • effective_perms(entry) -> AclPerms — Applies mask to entry per POSIX.1e rules. UserObj and Other are unaffected by mask.

FsFlags

struct FsFlags {
    immutable: bool,    // FS_IMMUTABLE_FL — no modifications at all
    append_only: bool,  // FS_APPEND_FL — write only in append mode
}

These flags are not overridden by root or capabilities — they apply unconditionally.

MacState / SeLinuxState / AppArmorState

Added in v0.3. Pre-gathered MAC (mandatory access control) layer state. Both probes default to Probe::Unknown when the corresponding subsystem is absent or unreadable.

struct MacState {
    selinux: Probe<SeLinuxState>,     // SELinux probe result
    apparmor: Probe<AppArmorState>,   // AppArmor probe result
}

SeLinuxMode

enum SeLinuxMode {
    Enforcing,   // Policy actively enforced; denials block access
    Permissive,  // Policy evaluated but denials logged, not enforced
    Disabled,    // SELinux compiled out or disabled at boot
}

SeLinuxState

struct SeLinuxState {
    mode: SeLinuxMode,          // Kernel enforcement mode at gather time
    subject_ctx: String,        // Subject security context, e.g. "unconfined_u:unconfined_r:unconfined_t:s0"
    target_ctx: String,         // Target file security context
    access_allowed: bool,       // AVC access decision pre-computed during gathering
}

AppArmorState

struct AppArmorState {
    profile_label: String,      // Profile label from procfs, e.g. "nginx (enforce)" or "unconfined"
}

operation — Operation Types

enum Operation {
    Read,     // Requires r
    Write,    // Requires w
    Execute,  // Requires x
    Delete,   // Checks w+x on parent
    Create,   // Checks w+x on parent
    Stat,     // Traverse-only (no file perm needed)
    Chmod,    // Ownership or CAP_FOWNER (v0.4)
    ChownUid, // Requires CAP_CHOWN (v0.4)
    ChownGid, // Owner-in-group or CAP_CHOWN (v0.4)
    SetXattr { namespace: XattrNamespace }, // Gated by namespace (v0.4)
}

Key methods:

  • checks_parent() -> booltrue for Delete and Create
  • target_component(walk_len) -> Option<usize> — Returns the index of the component to check. Last element for most ops, second-to-last for parent-directed ops.
  • is_metadata() -> boolv0.4. true for Chmod, ChownUid, ChownGid, SetXattr. Metadata ops bypass DAC/ACL mode-bit checks; the Metadata layer evaluates ownership + capability rules directly.

XattrNamespace

Added in v0.4. Namespace for setxattr(2) operations. Determines which capability is required.

enum XattrNamespace {
    User,            // user.* — owner or CAP_FOWNER
    Trusted,         // trusted.* — requires CAP_SYS_ADMIN
    Security,        // security.* — requires CAP_SYS_ADMIN
    SystemPosixAcl,  // system.posix_acl_access — owner or CAP_FOWNER
}

Derives Copy.

MetadataParams

Added in v0.4. Caller-supplied intent for metadata-change operations. Separate from SystemState (OS-gathered state). Missing fields cause the Metadata check layer to return Degraded.

struct MetadataParams {
    new_mode: Option<u32>,    // Target mode bits for Chmod; None → Degraded (not Fail)
    new_uid: Option<u32>,     // Target UID for ChownUid; None → Degraded (not Fail)
    new_gid: Option<u32>,     // Target GID for ChownGid; None → Degraded (not Fail)
}

Implements Default (all None). Passed to run_checks() and generate_fixes(). When a field is None for its corresponding operation, the metadata check layer returns Degraded rather than a hard Fail, signaling that the caller did not supply the required intent.

checks — Check Pipeline

Entry point: run_checks(state, &MetadataParams) -> CheckReport

Runs all core layers in order: mount → fs_flags → traversal → DAC → ACL → Metadata, then MAC layers. v0.4 breaking change: now takes &MetadataParams for metadata-op sub-checks. params carries caller-supplied intent for metadata operations (Chmod, ChownUid, ChownGid, SetXattr). Only the metadata layer reads it; all other layers ignore it. Pass MetadataParams::default() for non-metadata operations. capability_modify() (in caps_modify.rs) is applied to the DAC result as a post-check modifier. When capabilities is Probe::Unknown, capability_modify() falls back to uid==0 as a heuristic. DAC and ACL layers return Pass early for metadata operations (is_metadata() == true); the Metadata layer handles these via ownership + capability rules. MAC layers read pre-gathered state from SystemState.mac_state — no I/O at check time. Both MAC entries are always present: live checks when feature flags are enabled, Degraded stubs when absent.

CoreLayer enum

enum CoreLayer { Mount, FsFlags, Traversal, Dac, Acl, Metadata }

MacLayer enum

enum MacLayer { SeLinux, AppArmor }

LayerResult

enum LayerResult {
    Pass { detail: String, warnings: Vec<String> },
    Fail { detail: String, component_index: Option<usize> },
    Degraded { reason: String },
}

Warnings in Pass are advisory — they do not affect the pass/fail outcome. Used when a layer passes but with caveats, e.g., nosuid stripping suid/sgid bits on execute.

CheckReport

Contains core_results: EnumMap<CoreLayer, LayerResult> and mac_results: Vec<(MacLayer, LayerResult)>. Key methods:

  • is_allowed() -> bool — True if no layer is Fail
  • failed_layers() -> Vec<CoreLayer> — List of core layers that produced Fail results

Individual check functions

Each is a pure function: fn check_*(state: &SystemState) -> LayerResult (except check_metadata which also takes &MetadataParams)

Function Layer What it checks
check_mount() Mount read_only, noexec, nosuid on the target's filesystem (flags sourced from statvfs())
check_fs_flags() FsFlags immutable and append_only inode flags; for Delete operations, also checks append_only on the parent directory (mirrors the kernel's may_delete() behavior)
check_traversal() Traversal +x permission on every ancestor directory
check_dac() DAC Owner/group/other mode bits against subject identity. Returns Pass early for metadata operations (v0.4).
check_acl() ACL POSIX.1e ACL evaluation with mask application. Returns Pass early for metadata operations (v0.4).
check_metadata() Metadata v0.4. Implements setattr_prepare from fs/attr.c. Dispatches by operation: Chmod → owner or CAP_FOWNER; ChownUid → CAP_CHOWN; ChownGid → CAP_CHOWN or owner-in-target-group; SetXattr → namespace-dependent (user/posix_acl → owner/CAP_FOWNER, trusted/securityCAP_SYS_ADMIN). Returns Pass for non-metadata ops. Chmod warns when CAP_FOWNER bypasses ownership but caller lacks CAP_FSETID and is not in the file's group (kernel strips setgid bit per inode_init_owner). Capability resolution in the metadata layer treats Unknown/Inaccessible as zero (conservative) — unlike capability_modify()'s uid==0 root heuristic.
capability_modify() (post-DAC) In caps_modify.rs (split from dac.rs in v0.3). Three-way dispatch: Known(bitmask) checks CAP_DAC_OVERRIDE bit, Unknown with uid 0 uses root heuristic, Inaccessible leaves result unchanged. Root execute override requires at least one x bit set on any class.
check_selinux() SELinux (MAC) Reads state.mac_state.selinux. Disabled → Degraded; Permissive → Pass with warning; Enforcing → Pass/Fail based on pre-gathered access_allowed. Feature-gated (--features selinux); stub reads probe state when absent. v0.4: operation_to_selinux_perm() now pub and re-exported; metadata ops map to ("file", "setattr").
check_apparmor() AppArmor (MAC) Reads state.mac_state.apparmor. Unconfined → Pass; Complain → Pass with warnings (deny rules still enforced); Enforce → Degraded without libapparmor. v0.4: metadata ops in enforce mode return Degraded with a capability-rules hint (chown/fowner/sys_admin) rather than the generic libapparmor rebuild hint. Feature-gated (--features apparmor); stub reads probe state when absent.

fix — Fix Engine

Entry point: generate_fixes(report, state, &MetadataParams) -> FixPlan

Collects fixes for each failed layer, orders by layer position then impact ascending, runs cascade simulation, returns final plan. v0.4 breaking change: now takes &MetadataParams. params carries caller-supplied intent for metadata operations; only the metadata layer generator reads it to tailor fix targets. Pass MetadataParams::default() for non-metadata operations. Metadata layer failures produce GrantCap and Chown fix suggestions.

Fix

struct Fix {
    layer: CoreLayer,        // Which check layer this addresses
    action: FixAction,       // Structured action (rendered to shell command at output time)
    impact: u8,              // 1 (least privilege) to 6 (broadest)
    description: String,     // Human-readable
}

FixAction enum

enum FixAction {
    Chmod { path, mode_change },        // e.g., "o+x", "g+r"
    Chown { path, owner, group },       // Change ownership
    SetAcl { path, entry },             // e.g., "u:33:r"
    Remount { mountpoint, options },    // e.g., "rw", "exec"
    Chattr { path, flags },             // e.g., "-i", "-a"
    GrantCap { path, capability },      // e.g., "cap_fowner", "cap_sys_admin" (v0.4)
}

Rendered to shell commands only at output time. GrantCap with path: None renders as a descriptive recommendation rather than a concrete setcap command. Cascade simulator applies fixes semantically by mutating state struct copies.

FixPlan

struct FixPlan {
    fixes: Vec<Fix>,         // Ordered: layer position, then impact ascending
    warnings: Vec<String>,   // For high-impact fixes (score ≥ 5)
}

Submodules

Module Role
generators.rs Per-layer fix generation: mount_fixes(), fsflags_fixes(), traversal_fixes(), dac_fixes(), acl_fixes(), metadata_fixes() (v0.4). Private module (mod generators, not pub mod); not part of the public API surface — accessed only through generate_fixes().
scoring.rs Impact scoring (1–6 scale), needs_warning(impact) -> bool
cascade.rs simulate_cascade() — apply fix → re-run checks → prune redundant fixes. Max 5 iterations.
cascade/apply.rs apply_fix() — semantic state mutation per FixAction. Handles Chmod (mode bit manipulation), SetAcl (ACL entry merge), Chattr (flag removal), Remount (option flip), Chown (uid/gid mutation with ATTR_KILL_SUID/SGID semantics), and GrantCap (capability bitmask merge). v0.4: apply_chown(), apply_grant_cap().
commands.rs FixAction → shell command string rendering

Metadata Fix Strategies

metadata_fixes() produces least-privilege suggestions per operation type, ordered by ascending impact score:

Operation Fix Suggestions (impact ascending)
Chmod Chown file to subject UID (4), GrantCap cap_fowner (5)
ChownUid / ChownGid GrantCap cap_chown (5)
SetXattr user.* Chown file to subject UID (4)
SetXattr system.posix_acl_access Chown file to subject UID (4), GrantCap cap_fowner (5)
SetXattr trusted.* / security.* GrantCap cap_sys_admin (6) — triggers high-impact warning

GrantCap fixes are emitted with path: None (process binary unknown); they render as descriptive recommendations rather than concrete setcap commands. Pass --executable to make them actionable.

Cascade Apply Semantics

apply_chown() mirrors kernel notify_change(): clears S_ISUID on owner change, clears S_ISGID on group change only when S_IXGRP is set (ATTR_KILL_SUID/ATTR_KILL_SGID). apply_grant_cap() maps capability name strings (cap_fowner, cap_chown, cap_sys_admin, cap_fsetid) to bitmask bits and merges into state.subject.capabilities — promotes Unknown/Inaccessible to Known(bit).

test_helpers — Synthetic State Builders

Gated behind #[cfg(any(test, feature = "test-helpers"))]. Provides fluent builder API for constructing SystemState structs in tests without OS interaction.

Used by both whyno-core internal tests and whyno-cli dev-dependencies (via whyno-core = { features = ["test-helpers"] } in dev-deps).