# DF-2647 — hammer2_ioctl_pfs_get trusts on-media name_len (uint16) past a KKASSERT

## What

`sys/vfs/hammer2/hammer2_ioctl.c:494-496` (HAMMER2IOC_PFS_GET — the ioctl
behind `hammer2 pfs-list`):

```c
KKASSERT(ripdata->meta.name_len < sizeof(pfs->name));	/* INVARIANTS only */
bcopy(ripdata->filename, pfs->name, ripdata->meta.name_len);
pfs->name[ripdata->meta.name_len] = 0;
```

* `ripdata->meta.name_len` is **on-media, uint16_t** (`hammer2_disk.h:957`)
  — fully attacker-controlled by a crafted filesystem image, range 0..65535.
* `pfs->name` is 256 bytes (`hammer2_ioctl.h:103`, NAME_MAX+1).
* `KKASSERT` compiles out on production (non-INVARIANTS) kernels — it is the
  ONLY bound check. DragonFly's stock X86_64_GENERIC ships INVARIANTS ON, but
  release/production kernels are built without it.
* The ioctl data buffer is a **320-byte kmalloc'd M_IOCTLOPS heap allocation**
  (`sys/kern/sys_generic.c:674-676`, 512-byte malloc zone) — `pfs->name`
  ends exactly at the end of the request.

Consequences on a non-INVARIANTS kernel:

* **Kernel heap OOB WRITE** of `(name_len - 256)` bytes — up to 65279 — past
  `pfs->name[]`, with content taken from `inode+0x100` onwards. When the PFS
  inode is placed low in its 64KB DIO window the entire smear source is
  attacker-controlled image bytes (the forger stamps `DF2647!!`).
* Plus a single NUL write at `pfs->name[name_len]` (up to 65535 past).
* Plus an OOB **read** past the 256-byte `filename[]` field (same media
  window; in-window when the geometry is chosen as above).

On INVARIANTS kernels (incl. the stock guest): deterministic
`panic: assertion "ripdata->meta.name_len < sizeof(pfs->name)" failed in
hammer2_ioctl_pfs_get` — proof of reachability.

## Trust boundary (recorded honestly)

* The ioctl is root-gated (`caps_priv_check(SYSCAP_NOVFS_IOCTL)`,
  hammer2_ioctl.c:117-120) and mounting the image is root-gated
  (`vfs.usermount=0` on the guest).
* Threat model: **hostile filesystem image + routine root administration**
  (`hammer2 pfs-list`-equivalent, backup/forensics tooling, mount+inspect of
  untrusted media). This is the classic hammer2 hostile-media class the audit
  prioritizes; impact is ring-0 compromise of the inspecting machine, not a
  uid=0 jump for an unprivileged local user.

## Reproduce

1. Build the base image (guest, root): `sh mkbase2647.sh` — 64MB hammer2
   volume "testvol" + PFS "zz_pwn".
2. Forge (host): `python3 forge_df2647.py base2647.img h2_2647_0300.img 0x300`
   (surgical, 512-byte overrun) and `... 0xffff` (catastrophic, ~65KB
   overrun). Push to guest `/root/poc/`.
3. Build trigger: `cc -O2 -o pfsget_scan pfsget_scan.c` (also
   `pfslookup_victim.c`).
4. **Stock INVARIANTS kernel**: `vnconfig -c vn0 h2_2647_0300.img &&
   mount -t hammer2 /dev/vn0@testvol /mnt/h2 && ./pfsget_scan /mnt/h2 1`
   → instant KKASSERT panic (`panic_inv.txt`).
5. **Non-INVARIANTS kernel** (config DF2647_NOINV = X86_64_GENERIC minus
   `options INVARIANTS`):
   * surgical: `sh run_surgical.sh` — 40 scan iterations + 6 concurrent
     PFS_LOOKUP victim loops (same 512-byte M_IOCTLOPS zone); the victim's
     parked ioctl buffer shows the `DF2647!!` marker after copyout
     (controlled cross-object write); the corrupted free-lists eventually
     detonate as `Fatal trap 9 ... Stopped at _kmalloc` (`panic_noinv.txt`).
   * catastrophic: `sh run_catastrophic.sh` with the 0xFFFF image — single
     ~65KB controlled smear.
6. **Fix validation**: apply `fix.diff` (real bound check, clamps + kprintf),
   rebuild, rerun — no panic, no smear, kprintf reports the clamp, guest
   stays up (`fix_run.log`).

## Expected output

See VERDICT.md for the captured runs.
