# DF-2640 — PoC evidence pack

**Unguarded `hammer2_chain_dirent_test()` calls in the hammer2 xop lookup
loops: the first dereference of a corrupted (NULL-data) chain on every
resolve/unlink/rename — kernel NULL-deref panic, not remediated by DF-2617's
fix.**

`sys/vfs/hammer2/hammer2_xops.c` feeds freshly-locked chains from
`hammer2_chain_lookup()` straight into `hammer2_chain_dirent_test()` at
**xops.c:282** (nresolve), **:368** (unlink), **:610** (nrename source),
**:771** (nrename target-overwrite), with only `chain != NULL` checked.
`hammer2_chain_dirent_test()` (chain.c:5768-5793) dereferences
`chain->data->ipdata` (INODE entries, :5775) and `chain->data->buf`
(long-named DIRENT entries, :5784) with **no `chain->error` and no
`chain->data` check**. With the (still live) DF-2617 primitive —
`bref.data_off == 0` on a data-requiring chain makes
`hammer2_chain_load_data()` early-return success (chain.c:938-939), leaving
`chain->data == NULL`, `chain->error == 0` — this is a guaranteed NULL-deref.
DF-2617's verified fix.diff only guards the mount-path consumers (vfsops
label scan, iocom); it arms `chain->error` for these types but **nothing on
the xop lookup path reads it**, so this crash survives DF-2617's fix.

Key reachability detail proven on the guest: entries with names **> 64 bytes**
store their name in the dirent's data block, so `dirent_test` takes the
`bcmp(chain->data->buf, …)` branch — this xop-backend deref is the *first*
touch of the corrupted chain (short-named dirents are bref-embedded and never
touch `chain->data`).

## Reproduce

```sh
# host (pack dir): build base image in guest, forge on host, push
sh build.sh
# guest (root), stock INVARIANTS kernel #0:
sh /root/poc/df2640/trigger_U.sh   # mounts U.img, then:
                                  # su -m nobody -c 'rm /mnt/h2/pub/<80*p>'
# expected: Fatal trap 12, fault VA 0x0, "Stopped at bcmp+0xf0: movq (%rdi),%r8",
#           current process = Idle (xop backend thread), db> prompt, guest dead
```

With `fix.diff` applied (kernel #1, built in-guest with
`make -j6 nativekernel && make installkernel`), the same trigger returns
`rm: …: No such file or directory` (clean ENOENT), the guest stays up
(fix_run.log), and a full regression on the uncorrupted image (long+short
names, create/cp/mv/ln/rm as `nobody`, umount) passes.

Threat model: root mounts a crafted hammer2 image (USB stick / untrusted
volume); any unprivileged user with write permission on a directory (here
`nobody`, uid 65534, mode-777 `pub/`) then panics the machine with a single
`rm`. Same class/triggers as the verified DF-2616/DF-2617 pack forgers
(volhdr→sroot→PFS walk, CHECK_NONE ancestors, volhdr CRC32C recompute).

* Full narrative: **VERDICT.md**
* Machine verdict: **verdict.json** (schema: audit/persist_poc.py)
* Verified fix: **fix.diff** (chain.c dirent_test guards + xops.c hardening
  for the DF-2641 latent sinks; validated on a rebuilt kernel)
* Baseline crash: **panic.txt** (serial console, stock kernel #0);
  **panic_ls_variant.txt** — en-passant observation of the *frontend* readdir
  sink (vnops.c:727 `hammer2_xop_gdata()->buf`, "from ls", memcpy NULL-source)
  on the same forged image; that one belongs to hammer2_vnops.c, not this file.
