# DF-0772 — VERDICT

## Verdict: REPRODUCED — system-wide HAMMER2 DoS via infinite loop

**Status:** reproduced (DoS / infinite loop)
**Impact:** `dos` — kernel busy-loop holding global `hammer2_mntlk`, blocking all HAMMER2 mount/unmount/sync
**Confidence:** certain
**Fix status:** `fixed` — validated on single-fix kernel

---

## Mechanism

`hammer2_fixup_pfses()` (sys/vfs/hammer2/hammer2_vfsops.c:2366-2425) is called
during a read-write HAMMER2 mount (after `hammer2_recovery()` succeeds) to fix
mis-flagged PFS inodes. It scans the super-root inode's children:

```c
// VULNERABLE (lines 2389-2417)
chain = hammer2_chain_lookup(&parent, &key_next, KEY_MIN, KEY_MAX, &error, 0);
while (chain) {
    if (chain->bref.type != HAMMER2_BREF_TYPE_INODE)
        continue;                    // BUG: chain is NOT advanced
    if (chain->error) { ... }
    else if (...) { ... }
    chain = hammer2_chain_next(&parent, chain, &key_next, ...);  // SKIPPED
}
```

When `chain->bref.type != HAMMER2_BREF_TYPE_INODE` (e.g., a crafted image has
a `DATA` type=3 blockref as a child of the super-root), `continue` jumps back
to `while (chain)` without calling `hammer2_chain_next()`. Since `chain` is
unchanged, the loop re-evaluates the same type check → `continue` → spin
forever.

### Why the loop holds the system hostage

- `hammer2_vfs_mount()` acquires the global `hammer2_mntlk` lockmgr lock early
  (line ~1056, held through line ~1486). The infinite loop is inside this
  critical section.
- `hammer2_inode_lock(spmp->iroot, 0)` is held (line 2384 → 2423).
- ALL subsequent HAMMER2 mount/unmount operations block on `hammer2_mntlk`.
  The root filesystem is on HAMMER2 (`vbd0s1d on / (hammer2)`), so sync/unmount
  of the root FS is also blocked.

### Correct sibling: `hammer2_update_pmps` (lines 1553-1566)

```c
while (chain) {
    if (chain->error) { ... }
    else if (chain->bref.type != HAMMER2_BREF_TYPE_INODE) {
        kprintf("Non inode chain type %d under super-root\n", ...);
    } else { ... }
    chain = hammer2_chain_next(...);  // ALWAYS reached
}
```

The fix restructures `hammer2_fixup_pfses` to match this pattern.

### Reachability analysis

1. `hammer2_vfs_mount` (line ~1056): acquires `hammer2_mntlk`.
2. Volume header loaded and CRC-validated (hammer2_ondisk.c:485-560). Our
   corruption tool recomputes the 3 volume-header CRCs (iscsi_crc32) so this
   passes.
3. Super-root inode loaded via `hammer2_chain_lookup` on `hmp->vchain`
   (line ~1273). Our tool sets the blockref check method to `CHECK_NONE` so
   the modified inode data passes CRC.
4. `hammer2_recovery(hmp)` returns 0 ("no recovery needed" — confirmed in
   serial log).
5. `hammer2_fixup_pfses(hmp)` enters the infinite loop.

The trigger requires a crafted HAMMER2 image mounted RW by root
(`!hmp->ronly` gate at line 1334). RO mounts skip the vulnerable path.

## Reproduction evidence

### Baseline (unpatched #0 kernel)

Serial log (`serial_log_baseline.txt`) after RW mount of corrupted image:
```
hammer2_mount: device="/dev/vn0" label="DATA" rdonly=0
hammer2_ondisk: "/dev/vn0" zone=0 id=0 offset=0x0 size=0x20000000
hammer2_mount: "/dev/vn0": no recovery needed
[SILENCE — kernel spinning in hammer2_fixup_pfses]
```
- ssh `uptime` times out (guest unresponsive, all CPUs eventually starved
  by the spinning thread holding mntlk).
- Load average on an earlier run: 1.59 (one CPU fully consumed).
- A second HAMMER2 mount (clean image) also blocks on `hammer2_mntlk`.

### Patched (#1 kernel, fix applied)

Serial log (`serial_log_patched.txt`) after RW mount of the SAME corrupted image:
```
hammer2_mount: device="/dev/vn0" label="DATA" rdonly=0
hammer2_mount: "/dev/vn0": no recovery needed
hammer2: Non-inode chain type 3 under super-root, skipping   ← FIX FIRED
Non inode chain type 3 under super-root                       ← hammer2_update_pmps
HAMMER2: VOLDATA DUMP
HAMMER2: INITIATE SPANs                                       ← mount PROCEEDED PAST fixup
```
- Load average: 0.01 (CPU NOT spinning — no infinite loop).
- ssh responsive; `echo`, `uptime`, `mount` all return immediately.

The before/after contrast is unambiguous: the infinite loop is eliminated.

## The corruption tool (`corrupt_h2.c`)

A userspace C tool that:
1. Reads the volume header (first 64KB of the image).
2. Follows `sroot_blockset.blockref[0].data_off` to the super-root inode data.
3. Changes the first child blockref's `type` byte from INODE(1) to DATA(3).
4. Sets the super-root blockref's check method to CHECK_NONE (so the modified
   inode data isn't rejected by the CRC check during chain loading).
5. Recomputes the 3 volume-header CRCs (iscsi_crc32 from kernel libkern).

Compiled with: `cc -o corrupt_h2 corrupt_h2.c /usr/src/sys/libkern/icrc32.c`

## Fix validation

### fix.diff

Restructures the `if (... != INODE) continue;` + `if (chain->error)` into an
`if (... != INODE) { kprintf(...); } else if (chain->error)` chain, so
`hammer2_chain_next()` at the bottom of the loop is always reached. Adds a
diagnostic kprintf matching `hammer2_update_pmps`'s style.

### Build + install

- `make -j6 quickkernel KERNCONF=X86_64_GENERIC` → rc=0 (warm obj, ~90s).
- `make installkernel` + copy `kernel.stripped` → `/boot/kernel/kernel`.
- Booted: `DragonFly 6.5-DEVELOPMENT #1: Thu Jul  9 20:29:43 UTC 2026`.

### Before/after

| Kernel | Serial log after RW mount | Load avg | ssh responsive |
|--------|--------------------------|----------|----------------|
| #0 (unpatched) | stops at "no recovery needed" | 1.59+ | NO (times out) |
| #1 (patched) | "Non-inode type 3, skipping" → "INITIATE SPANs" | 0.01 | YES |

**Conclusion:** fix closes the bug. The infinite loop in `hammer2_fixup_pfses`
is eliminated; the mount proceeds past the fixup function on the patched kernel.

## Notes

- The mount may still be slow on the patched kernel for reasons unrelated to
  this bug (HAMMER2 cluster/iocom messaging setup on a vn-backed image with
  no cluster peers). This is a separate behavior, not the infinite loop.
- The trigger requires root to mount (PR:H). A real-world scenario: an admin
  mounts a malicious HAMMER2 image (e.g., from an untrusted USB device or a
  downloaded filesystem image).
- RO mounts are unaffected (gated by `!hmp->ronly`).
