# DF-5843 — hammer2_xop_inode_chain_sync KKASSERT(0) panic via crafted image

`panic: assertion "0" failed in hammer2_xop_inode_chain_sync at /usr/src/sys/vfs/hammer2/hammer2_xops.c:1582`

## What this is

`hammer2_xop_inode_chain_sync()` (sys/vfs/hammer2/hammer2_xops.c:1536) is the
backend worker that implements file truncation bookkeeping.  When the frontend
flags a shrink (`xop->meta.size < xop->osize`, :1560), it walks **every child
chain of the file inode from `lbase` to `HAMMER2_KEY_MAX`** (:1570-1594) and
switches on `chain->bref.type` (:1579).  Only `HAMMER2_BREF_TYPE_DATA` is
handled; `DIRENT` and `INODE` hit `KKASSERT(0)` (:1582) — a *guaranteed kernel
panic*, compiled into release kernels (KKASSERT is always active).

`hammer2_chain_lookup()` does not filter child bref types
(hammer2_chain.c:2482-2511: it scans the inode's `u.blockset.blockref[]` and
returns *any* entry found), and the loop runs **regardless of `chain->error`**
— so an INODE- or DIRENT-typed blockref planted under a regular-file inode
(on a crafted or corrupted image) reaches the assert on the next
`ftruncate()`/close of that file.

## Threat model

* Attacker crafts a hammer2 image (offline image editor — see `mkbadimg.c`)
  in which one of a regular file's direct data blockrefs claims
  `type = HAMMER2_BREF_TYPE_INODE`.
* The image gets mounted (root, or a user when `vfs.usermount` policies
  allow).  Crafted-image bugs are also reachable on legitimately corrupted
  media.
* **Any unprivileged user** who can open the file O_RDWR and ftruncate it
  (or simply open+close it if the frontend already flagged RESIZED) panics
  the kernel: full local DoS.

## Reproduce (on the audit QEMU guest, as root)

```
./build.sh          # compiles mkbadimg + trig (needs /usr/src in the guest)
./run.sh            # builds pristine image, patches it, mounts, triggers
```

`run.sh` does:

1. `dd` a 2GB image, `newfs_hammer2`, mount, write a 128KB file
   `BIGTGTNAME` (two direct DATA blockrefs at keys 0 and 0x10000), unmount.
2. `./mkbadimg /root/h2.img BIGTGTNAME` — flips the second blockref's type
   DATA(3) → INODE(1), retargets its `data_off` radix to 10 (1024B inode),
   and repairs the whole xxhash64/icrc check tree upward (file inode →
   PFS-root inode → superroot inode → volume header `sroot_blockset` +
   `icrc_sects[6]`/`[7]` + `icrc_volheader`) via a fixpoint loop, so the
   crafted media still passes every validation the kernel performs.
3. Re-attach and mount the crafted image; `./trig /mnt/BIGTGTNAME 65536`
   (open O_RDWR + ftruncate to 64KB + close).

## Expected output

Baseline (vulnerable kernel): after `open ok` / `ftruncate -> 65536` the
console shows

```
panic: assertion "0" failed in hammer2_xop_inode_chain_sync at /usr/src/sys/vfs/hammer2/hammer2_xops.c:1582
hammer2_xop_inode_chain_sync() at hammer2_xop_inode_chain_sync+0x161
hammer2_primary_xops_thread() at hammer2_primary_xops_thread+0x280
```

and the system drops into `db>` (dead).  See `panic.txt`, `run.log`.

Patched kernel (fix.diff): no panic; `trig` prints
`SURVIVED: no panic (chain_sync did not hit KKASSERT)` and the guest stays
up (truncate is failed gracefully with HAMMER2_ERROR_CHECK).

## Files

* `mkbadimg.c` — crafted-image patcher (self-contained; links the guest's
  `/usr/src/sys/vfs/hammer2/xxhash/xxhash.c` + `/usr/src/sys/libkern/icrc32.c`)
* `trig.c` — open/ftruncate/close trigger (unprivileged once mounted)
* `build.sh`, `run.sh` — exact commands
* `build.log`, `run.log`, `panic.txt` — decisive untrimmed output
* `env.txt` — guest environment
* `fix.diff` — one-line fix (KKASSERT(0) → HAMMER2_ERROR_CHECK)
* `VERDICT.md`, `manifest.json`, `verdict.json` — verdict + narrative
