**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`). No runtime dependencies.
```bash
# Build from source
cargo build --release --target x86_64-unknown-linux-musl
# Optional: install CAP_DAC_READ_SEARCH for full coverage without sudo
sudo whyno caps install
```
### Privilege tiers
| **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
```bash
whyno <subject> <operation> <path> [flags]
```
### Subject formats
| 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
| `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)
- `--json` and `--explain` are **mutually exclusive**
---
## Example Output
```bash
whyno nginx read /var/log/app/current.log
Subject: nginx (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)
```
---
## Permission Layers Checked (v0.1)
All 5 layers run unconditionally — no short-circuiting. This ensures the fix engine sees the full picture.
| 1 | **Mount options** | `ro`, `noexec`, `nosuid` from `/proc/self/mountinfo` |
| 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 |
### Not checked in v0.1
SELinux, AppArmor, Linux capabilities, systemd sandboxing, user namespaces, seccomp. When SELinux or AppArmor is detected, a stderr warning is printed.
---
## Fix Suggestions
Fixes are ranked by **security impact score** (1 = least privilege, 6 = broadest blast radius):
| 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
| `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.
---
## Capability Management
```bash
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).