# DF-5836 — hammer2 duplicate-inum inode stomp in hammer2_inode_create_normal

## What

`sys/vfs/hammer2/hammer2_inode.c` `hammer2_inode_create_normal()` assumes the
freshly allocated `inum` never matches an existing in-memory inode. The only
guard is a `KKASSERT` (compiled out on production kernels). When the pmp inode
allocator is seeded from on-media `iroot meta.pfs_inum` (attacker-controlled on
a crafted image; stale after an iroot-flush-error crash on a legit fs),
`hammer2_inode_get()` returns the LIVE existing inode and create_normal
stomps its meta-data wholesale (type, uid/gid, mode, nlinks=1, DIRECTDATA)
while the old `meta.size` survives, sets `HAMMER2_INODE_CREATING` on a
fully-linked inode, and repoints its chains to new detached ones — the next
filesystem sync double-inserts a chain into the media topology at the same key.

## Reproduce

1. **Build pristine image** (root in guest): `./build_image.sh`
   → origfile at inum 1024 (= HAMMER2_INODE_START), fs label TESTFS,
   PFS-root inode block at media offset 0x8800c00 (see craft_image.py asserts).
2. **Pull, craft, push** (host):
   `scp dfbsd:/root/h2.img .` then `python3 craft_image.py h2.img h2c.img`
   then `scp h2c.img dfbsd:/root/h2c.img`
   (craft.py rewrites pfs_inum 1026→1023, disables the two parent-link
   check codes by setting bref methods to 0x00, recomputes the three
   volume-header CRC-32Cs — pure-python CRC-32C, self-tested).
3. **Trigger** (root in guest, fresh boot): `./run.sh`
   - mount TESTFS
   - `stat /mnt/h2/origfile` materializes inum 1024 into the pmp inum hash
   - `mkdir /mnt/h2/boomdir` → first `hammer2_trans_newinum()` returns
     1024 → `hammer2_inode_get()` finds the live inode → boom.

## Expected

* Stock (INVARIANTS) kernel — **panic**:
  `assertion "(nip->flags & HAMMER2_INODE_CREATING) == 0 && nip->cluster.nchains == 0" failed in hammer2_inode_create_normal at /usr/src/sys/vfs/hammer2/hammer2_inode.c:1159`
  trace: `hammer2_inode_create_normal+0x3a9 ← hammer2_vop_nmkdir ← vop_nmkdir ← kern_mkdir ← sys_mkdir`
  (see run.log / boot_panic_raw.txt)
* Production (non-INVARIANTS) kernel: no panic; the stomp proceeds silently —
  origfile's in-memory meta is replaced by the new directory's (type/uid/mode/
  nlinks/ctime/DIRECTDATA flip) and the next sync attempts the duplicate
  chain insertion (analysis; not run here because the guest kernel is INVARIANTS).
* **Patched kernel (fix.diff)**: mkdir fails `EEXIST`, dmesg shows
  `hammer2: duplicate inode number 1024 detected, create refused`,
  origfile byte-identical, sync/umount clean (run.fixed.log).

Mounting a crafted image requires root (or `vfs.usermount=1` + user-owned
device); after mount, the trigger itself is a plain `mkdir` (any user with
write permission on the mount).
