5 releases
| new 0.2.0 | Feb 20, 2026 |
|---|---|
| 0.1.4 | Feb 14, 2026 |
| 0.1.3 | Feb 14, 2026 |
| 0.1.2 | Feb 14, 2026 |
| 0.1.1 | Feb 14, 2026 |
#164 in Unix APIs
415KB
10K
SLoC
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]
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 |
Flags
--json— structured JSON output (versioned schema, CI-friendly)--explain— verbose resolution chain with per-component raw data--no-color— disable ANSI color (also respectsNO_COLORenv 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 kernelfaccessat2(AT_EACCESS); valid only when subject is the calling user and kernel >= 5.8; mismatches reported to stderr; runs automatically in debug builds--jsonand--explainare 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).
Dependencies
~3.5–6.5MB
~128K SLoC