# DF-0815 — Missing privilege check on HAMMER2 BULKFREE_SCAN/ASYNC + DEBUG_DUMP ioctls

## Claim (Medium, CWE-862 Missing Authorization)
`hammer2_ioctl()` (`sys/vfs/hammer2/hammer2_ioctl.c:83`) computes
`error = caps_priv_check(cred, SYSCAP_NOVFS_IOCTL);` and **every** admin ioctl
case guards its handler with `if (error == 0)`. But three cases do not:

| Line | Case | Code |
|------|------|------|
| 144-146 | `HAMMER2IOC_BULKFREE_SCAN`  | `error = hammer2_ioctl_bulkfree_scan(ip, data);` |
| 147-149 | `HAMMER2IOC_BULKFREE_ASYNC` | `error = hammer2_ioctl_bulkfree_scan(ip, NULL);` |
| 154-156 | `HAMMER2IOC_DEBUG_DUMP`     | `error = hammer2_ioctl_debug_dump(ip, *(u_int*)data);` |

They assign directly to `error`, **ignoring the privilege check**. An
unprivileged user holding any fd on a hammer2 mount can invoke them.

## Reachability on the audit guest
Root filesystem is **hammer2** (`vbd0s1d on / (hammer2, local)`), so **any** fd
on `/` (e.g. `open("/tmp", O_RDONLY)`) routes ioctls through `vn_ioctl` →
`hammer2_vop_ioctl` (`hammer2_vnops.c:2264`) → `hammer2_ioctl`. No special
device node, no mount permission — just an fd the unprivileged user already has.

## Impact
- **DEBUG_DUMP** — `kprintf`s kernel `%p` pointers (`chain`, `parent`,
  `chain->data`) of hammer2 internal chains via `hammer2_dump_chain`
  (`hammer2_chain.c:5812-5829`). With `security.unprivileged_read_msgbuf=1`
  (default) the unprivileged user reads these via `sysctl kern.msgbuf` →
  **kernel address leak / KASLR bypass**. `flags` is user-controlled (`*(u_int*)data`).
- **BULKFREE_SCAN** — takes `hmp->bflock` EXCLUSIVE (`:1110`), syncs **every**
  PFS on the media (`:1118-1130`), then runs a full-media bulkfree scan
  (`:1164`). Sustained kernel work / **DoS** from an unprivileged user.
- **BULKFREE_ASYNC** — same handler; would also DoS.

## Build / Run
```
cc -o df0815 df0815.c
./df0815                 # as unprivileged user (uid 1001 maxx)
```

## Expected output
- **Bug present (unpatched `#0`):**
  - `DEBUG_DUMP rc=0` (priv check bypassed; `%p` pointers appear in msgbuf)
  - `BULKFREE_ASYNC rc=EINVAL` (handler NULL-check at `:1103`, NOT `EPERM`)
- **Fixed (patched):** both return `EPERM` (errno=1).

## Notes on test discipline
- `BULKFREE_SCAN` with real data is **NOT** exercised live — it would run the
  full bulkfree scan (DoS) and wedge the guest before fix-validation could
  proceed. Its identical missing `if (error == 0)` guard is proven by code
  inspection (`hammer2_ioctl.c:144-145`) and by the DEBUG_DUMP/ASYNC probes.
- `BULKFREE_ASYNC` passes NULL internally → handler returns EINVAL at `:1103`
  without running the scan, making it a safe probe (EINVAL ≠ EPERM ⇒ priv
  check bypassed).
- `DEBUG_DUMP` with `flags=0` is safe (only top-level inode chains, no
  recursion).
