whyno-cli 0.5.0

Linux permission debugger
whyno-cli-0.5.0 is not a library.

CI crates.io MSRV License

whyno is a Linux permission debugger. It answers the question: "Why can't this user do this to this file?"

Given a subject (user, UID, PID, or service), an operation (read, write, execute, delete, create, stat, chmod, chown-uid, chown-gid, setxattr), and a filesystem path, whyno checks every permission layer from mount options down to MAC policy — then tells you exactly what's blocking and how to fix it with the least-privilege change.

Installation

whyno ships as a single static binary (x86_64-unknown-linux-musl, aarch64-unknown-linux-musl, armv7-unknown-linux-musleabihf, riscv64gc-unknown-linux-musl). No runtime dependencies.

curl -LsSf https://releases.gnu.foo/whyno/install.sh | sh

Or build from source:

# Build from source (x86_64)
cargo build --release --target x86_64-unknown-linux-musl

# Build for aarch64 (requires cross)
cross build --release --target aarch64-unknown-linux-musl

# Build all release targets via justfile
just release

# Optional: install CAP_DAC_READ_SEARCH for full coverage without sudo
sudo whyno caps install

Privilege tiers

Tier Setup Coverage
Unprivileged None Partial — limited to paths the running user can traverse
Self-install caps sudo whyno caps install (once) Full — CAP_DAC_READ_SEARCH granted via raw setxattr(), zero external deps
sudo sudo whyno ... per invocation Full

In unprivileged mode, inaccessible checks are marked [SKIP] (never false-green). A one-time hint suggests elevated options.

Usage

whyno <subject> <operation> <path> [flags]

Subject formats

Format Example Resolution
Bare username whyno nginx read /path /etc/passwd/etc/group
Bare number whyno 33 read /path UID lookup in /etc/passwd
user: prefix whyno user:nginx read /path Explicit username
uid: prefix whyno uid:33 read /path Explicit UID
pid: prefix whyno pid:1234 read /path /proc/<pid>/status
svc: prefix whyno svc:postgres read /path systemd → MainPID → /proc

Operations

Operation Checks Notes
read r on target File contents or directory listing
write w on target Modify, truncate
execute x on target Run binary or traverse directory
delete w+x on parent Redirects check to parent directory
create w+x on parent Redirects check to parent directory
stat Traverse only "Can I see this exists?" — no file perm needed
chmod Ownership / CAP_FOWNER Change mode bits — kernel setattr_prepare rules
chown-uid CAP_CHOWN Change file owner UID — always requires capability
chown-gid Owner-in-group / CAP_CHOWN Change file group — owner must be member of target group, or CAP_CHOWN
setxattr Per-namespace rules user.*: owner or CAP_FOWNER; trusted.*/security.*: CAP_SYS_ADMIN

Flags

  • --json — structured JSON output (versioned schema, CI-friendly)
  • --explain — verbose resolution chain with per-component raw data
  • --no-color — disable ANSI color (also respects NO_COLOR env var)
  • --with-cap <CAP> — inject a capability for hypothetical queries; repeatable. Accepted names: CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH, CAP_FOWNER, CAP_LINUX_IMMUTABLE
  • --new-mode <OCTAL> — target mode for chmod operations (e.g. 0755)
  • --new-uid <UID> — target UID for chown-uid operations
  • --new-gid <GID> — target GID for chown-gid operations
  • --xattr-key <KEY> — extended attribute key for setxattr operations (e.g. user.custom)
  • --self-test — cross-check whyno's result against kernel faccessat2(AT_EACCESS); valid only when subject is the calling user and kernel >= 5.8; mismatches reported to stderr; runs automatically in debug builds
  • --json and --explain are mutually exclusive

Example Output

whyno nginx read /var/log/app/current.log

  Subject: uid=33, gid=33, groups=[33]
  Operation: read
  Target: /var/log/app/current.log

  [PASS] Mount options      — rw on /var (ext4)
  [PASS] Filesystem flags   — no immutable/append-only
  [FAIL] Path traversal     — /var/log/app: o-x (other has no execute)
         Fix: chmod o+x /var/log/app  [impact: 3/6]
  [FAIL] DAC permissions    — mode 0640 owner=root group=root, nginx is other
         Fix: setfacl -m u:nginx:r /var/log/app/current.log  [impact: 1/6]
  [SKIP] POSIX ACLs         — no ACL on target (would pass after DAC fix)
  [PASS] Metadata           — not a metadata operation
  [SKIP] SELinux            — SELinux state not gathered
  [SKIP] AppArmor           — AppArmor state not gathered

For pid:N and svc: subjects with readable capabilities, the subject line includes caps=0x... (e.g. caps=0x0000000000000000).

Permission Layers Checked (v0.5)

All layers run unconditionally — no short-circuiting. This ensures the fix engine sees the full picture.

Order Layer What it checks
1 Mount options ro, noexec, nosuid via statvfs()
2 Filesystem flags immutable, append-only via ioctl(FS_IOC_GETFLAGS)
3 Path traversal +x on every ancestor directory from / to target
4 DAC permissions Owner/group/other rwx mode bits + supplementary groups
5 POSIX ACLs Named user/group entries with mask application per POSIX.1e
6 Metadata Ownership and capability checks for chmod/chown/setxattr via kernel setattr_prepare semantics
7 SELinux Pure function over state.mac_state; when state is not gathered, reports [SKIP] SELinux state not gathered
8 AppArmor Pure function over state.mac_state; when state is not gathered, reports [SKIP] AppArmor state not gathered

Not checked in v0.5

systemd sandboxing directives, seccomp BPF filters, full user namespace UID/GID mapping, per-domain SELinux permissive state (system-wide only). MAC layers (SELinux, AppArmor) run as pure functions over gathered state — when the gather step cannot read MAC state (e.g. securityfs not mounted, or SELinux disabled), the layer degrades to [SKIP] with a descriptive message.

Fix Suggestions

Fixes are ranked by security impact score (1 = least privilege, 6 = highest risk):

Score Fix class Example
1 ACL grant (specific user) setfacl -m u:nginx:r file
2 Group change / ACL group grant chown :www-data file
3 Permission bit (group) chmod g+r file
4 Permission bit (other) chmod o+r file
4 Chown (transfer ownership) chown deploy:deploy file
5 Remove filesystem flag chattr -i file
5 Grant capability (CAP_FOWNER / CAP_CHOWN) setcap cap_fowner+ep /usr/bin/app
6 Remount filesystem mount -o remount,rw /var
6 Grant capability (CAP_SYS_ADMIN) setcap cap_sys_admin+ep /usr/bin/app

Fixes with score ≥ 5 include a warning. chmod 777 and o+rwx are never suggested.

When multiple layers block, an ordered fix plan is generated (outermost layer first). Cascade simulation re-runs checks after each hypothetical fix to prune redundant suggestions.

Exit Codes

Code Meaning
0 All layers pass — operation allowed
1 At least one layer blocks — operation denied
2 Internal error — couldn't complete checks

Same codes apply to --json mode. Degraded layers (unprivileged mode) do not force a non-zero exit.

JSON Schema

whyno schema prints the auto-generated JSON Schema for --json output (doc comments are lowercased per schemars convention). Use to validate consumer tooling against the current schema contract.

Capability Management

sudo whyno caps install    # Set CAP_DAC_READ_SEARCH on the binary
sudo whyno caps uninstall  # Remove the capability
whyno caps check           # Verify current state

Uses raw setxattr() / getxattr() / removexattr() syscalls with VFS cap v2 format (20 bytes). No libcap or setcap dependency. Filesystem must support extended attributes (ext4, xfs, btrfs — yes; NFS, FAT — no).