# DF-2666 — HAMMER2IOC_RECLUSTER leaks the holdfp() file reference on error returns

## Finding
`hammer2_ioctl_recluster()` (sys/vfs/hammer2/hammer2_ioctl.c:212-236) takes a
file reference with `holdfp(curthread, recl->fd, -1)` and transfers it into
the kernel iocom **only on the two success branches**:

```c
fp = holdfp(curthread, recl->fd, -1);
if (fp) {
        error = VFS_ROOT(ip->pmp->mp, &vproot);
        if (error == 0) {
                cluster = &ip->pmp->iroot->cluster;
                if (cluster->focus != NULL) {
                        hammer2_cluster_reconnect(cluster->focus->hmp, fp);  /* ref transferred */
                        error = 0;
                } else if (cluster->nchains == 1 &&
                           cluster->array[0].chain != NULL) {
                        hammer2_cluster_reconnect(...array[0]..., fp);       /* ref transferred */
                        error = 0;
                } else {
                        kprintf(...); error = EINVAL;                         /* LEAK: no fdrop(fp) */
                }
                vput(vproot);
        }                                                                       /* LEAK: no fdrop(fp) on VFS_ROOT error */
} else {
        error = EINVAL;
}
return error;
```

`hammer2_cluster_reconnect()` (hammer2_iocom.c:73-85) and its backend
`kdmsg_iocom_reconnect()` (kern_dmsg.c:128-131) implement a one-way
ownership contract — *"The caller must ref the fp for us ... We own that ref
now"* — with no failure path back to the caller.  Both error returns in the
ioctl therefore leak one `struct file` reference per call (each pinned ref
keeps the file structure and everything it references — vnode/socket and
buffers — alive forever).

Privilege: root-only (`caps_priv_check(cred, SYSCAP_NOVFS_IOCTL)` at
hammer2_ioctl.c:83 gates HAMMER2IOC_RECLUSTER at :89-91).  Severity: Low.

## What the guest run established (stock INVARIANTS kernel #0)

1. **Success-path ref accounting is balanced** — 200 consecutive
   HAMMER2IOC_RECLUSTER calls on a healthy mount: 200× OK, 0× EINVAL,
   `kern.openfiles` delta **0** (each call transfers the new ref; the next
   `kdmsg_iocom_reconnect` drops the previous `iocom->msg_fp`).
2. **The leak branch was not enterable** on a healthy guest mount —
   `iroot->cluster.focus` is non-NULL for a successfully mounted local PFS,
   and `VFS_ROOT()` does not fail there; we found no way to construct the
   degraded-cluster precondition (`focus == NULL && nchains != 1`) from
   userland.  The leak itself is certain by code inspection.
3. **Bonus pass-2 stress (shutdown/retract ordering)** — ~1250 rapid
   reconnect cycles (this run + the aborted sizing run): each cycle kills
   and reaps both iocom threads, tears down the auto-LNK_CONN state through
   the synthesized-failure path (kern_dmsg.c:1391-1407) — which re-enters
   `hammer2_autodmsg`'s `LNK_CONN|CREATE|REPLY` block (tcmd is derived from
   the state's icmd, kern_dmsg.c:1050-1058; observed as per-cycle
   "VOLDATA DUMP / INITIATE SPANs / CONN WAS TERMINATED" console lines) —
   and re-creates the threads.  No wedge, no panic, unmount clean.

## Files
- `df2666_trigger.c`   trigger (mount, 200× RECLUSTER, kern.openfiles deltas)
- `run_df2666.sh`      guest-side orchestration (vn image, newfs, build, run)
- `build.sh`/`run.sh`  host-side wrappers
- `run.log`            decisive guest run (trigger stdout + console deltas)
- `build.log`          cc output
- `env.txt`            guest uname/env
- `fix.diff`           the fix (fdrop on failure returns)
- `VERDICT.md`         full narrative
- `manifest.json`, `verdict.json`

## Build & run (from the repo root)
```
scp -F dfbsd-qemu/config findings/poc/DF-2666/df2666_trigger.c findings/poc/DF-2666/run_df2666.sh dfbsd:/root/poc/df2666/
dfbsd-qemu/vm.sh run_root 'sh /root/poc/df2666/run_df2666.sh'
```
Stock kernel: `RECLUSTER_OK=200 RECLUSTER_EINVAL=0 DELTA=0` (no leak on the
healthy path; leak branch not enterable).  A kernel with the leak branch
reachable would show `RECLUSTER_EINVAL>0` and `DELTA == RECLUSTER_EINVAL`.
