# DF-0772 — Infinite loop in hammer2_fixup_pfses on non-INODE blockref

## Summary

`hammer2_fixup_pfses()` in `sys/vfs/hammer2/hammer2_vfsops.c` enters an
infinite loop when it encounters a non-INODE blockref under the super-root
inode during a read-write mount. The loop body checks
`chain->bref.type != HAMMER2_BREF_TYPE_INODE` and executes `continue`,
bypassing the `hammer2_chain_next()` advance at the bottom of the loop. Since
`chain` is unchanged, the loop re-evaluates the identical type check and
`continue`s forever — a tight kernel busy-loop.

The spin holds the global `hammer2_mntlk` lockmgr lock and the `spmp->iroot`
inode lock, blocking **all** subsequent HAMMER2 mount/unmount operations
system-wide (including root-FS sync). Impact: denial of service triggered by
mounting a crafted HAMMER2 filesystem image read-write (root-mount threat
model, CVSS PR:H).

## The bug (line-by-line)

```c
// sys/vfs/hammer2/hammer2_vfsops.c:2389-2417  (VULNERABLE)
while (chain) {
    if (chain->bref.type != HAMMER2_BREF_TYPE_INODE)
        continue;                    // <-- BUG: chain NOT advanced
    if (chain->error) {
        ...
    } else if (...) {
        ...
    }
    chain = hammer2_chain_next(...); // <-- NEVER REACHED for non-INODE
}
```

The sibling function `hammer2_update_pmps()` (same file, lines 1553-1566)
shows the correct pattern: the type check is inside an `if/else-if` chain so
`hammer2_chain_next()` at the bottom is **always** executed.

## Trigger

1. Create a valid HAMMER2 image: `dd if=/dev/zero ... && newfs_hammer2`.
2. Corrupt one child blockref of the super-root inode: change its `type`
   byte from `INODE (1)` to `DATA (3)` (or any non-INODE, non-EMPTY value),
   and set the super-root blockref's check method to `CHECK_NONE` so the
   modified inode data passes CRC validation during volume-header loading.
3. `vnconfig -c vn0 image.img && mount_hammer2 /dev/vn0 /mnt/x` (RW).
4. The mount syscall enters `hammer2_vfs_mount → hammer2_fixup_pfses`,
   hits the non-INODE child, and spins forever.

## Fix

Restructure the type check into an `if/else-if` chain (mirroring
`hammer2_update_pmps`) so `hammer2_chain_next()` is always reached. See
`fix.diff`.

## Files

| File | Description |
|------|-------------|
| `corrupt_h2.c` | Image-corruption + CRC-recomputation tool (compiles with kernel `icrc32.c`) |
| `trigger.sh` | End-to-end trigger: newfs → corrupt → vnconfig → mount (RW) |
| `fix.diff` | git-apply-able one-hunk fix |
| `serial_log_baseline.txt` | Unpatched #0 kernel: serial log stops at "no recovery needed" (infinite loop) |
| `serial_log_patched.txt` | Patched #1 kernel: shows "Non-inode chain type 3, skipping" + "INITIATE SPANs" (fixed) |
| `fix_build.log` | Full `make nativekernel` output (rc=0) |
| `VERDICT.md` | Detailed analysis |

## Reproduce

```sh
# On the DragonFlyBSD guest as root:
dd if=/dev/zero of=/tmp/h2.img bs=1m count=512
newfs_hammer2 /tmp/h2.img
cc -o /tmp/corrupt_h2 corrupt_h2.c /usr/src/sys/libkern/icrc32.c
/tmp/corrupt_h2 /tmp/h2.img corrupt    # flip child[0] type INODE→DATA
vnconfig -c vn0 /tmp/h2.img
mkdir -p /mnt/h2test
mount_hammer2 /dev/vn0 /mnt/h2test     # RW mount → HANGS on unpatched kernel
```
