Readme
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), and a filesystem path, whyno checks every permission layer from mount options down to POSIX ACLs — 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 ). No runtime dependencies.
# 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
# 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]
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
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
--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 )
[SKIP] SELinux — not compiled in (rebuild with -- features selinux )
[SKIP] AppArmor — not compiled in (rebuild with -- features apparmor )
For pid: N and svc: subjects with readable capabilities, the subject line includes caps= 0x... (e.g. caps= 0x0000000000000000 ).
Permission Layers Checked (v0.2)
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
SELinux
In-kernel AVC query via selinux_check_access ( ) (--features selinux )
7
AppArmor
Profile mode detection via securityfs (--features apparmor )
Not checked in v0.2
systemd sandboxing directives, seccomp BPF filters, full user namespace UID/GID mapping, per-domain SELinux permissive state (system-wide only). MAC layers require --features selinux or --features apparmor at build time.
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
5
Remove filesystem flag
chattr - i file
6
Remount filesystem
mount - o remount, rw / var
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. 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).