# DF-5843 VERDICT — REPRODUCED (panic, local DoS via crafted hammer2 image)

**Status:** reproduced · **Impact:** panic (kernel panic → full local DoS) ·
**Confidence:** certain · **Attempts:** 4 (3 patcher iterations, 1 decisive trigger)

## The bug

`hammer2_xop_inode_chain_sync()` — sys/vfs/hammer2/hammer2_xops.c:1536 — is the
backend worker implementing truncation bookkeeping. On a shrink
(`xop->meta.size < xop->osize`, :1560) it scans **all children of the file
inode** from `lbase` to `HAMMER2_KEY_MAX` (:1570-1594, NODATA|NODIRECT) and
switches on `chain->bref.type` (:1579). DIRENT/INODE types hit
`KKASSERT(0)` at **:1582** — always compiled in on DragonFly.

Why it is reachable with crafted media:

1. `hammer2_chain_lookup()` scans the inode's `u.blockset.blockref[0..3]`
   raw and returns a chain for **any** bref type present
   (hammer2_chain.c:2482-2511); `HAMMER2_LOOKUP_NODIRECT` only short-circuits
   the DIRECTDATA-embedded case (hammer2_chain.c:2496-2502).
2. The truncate loop does **not** check `chain->error` before the switch, so
   even a checksum-errored planted chain reaches the assert.
3. Regular files legitimately never have non-DATA children, so the media is
   the only way to get one there — and media is attacker-controlled once you
   accept a crafted/corrupted image (mount by root, or via usermount
   policies; `vfs.usermount=0` on the audit guest, so the mount step is
   privileged — the *trigger* itself, `ftruncate()`, is unprivileged).

Frontend reachability: `hammer2_truncate_file()` sets HAMMER2_INODE_RESIZED
(hammer2_vnops.c:1266-1270); `hammer2_inode_chain_sync()` (hammer2_inode.c:1695)
issues the xop synchronously on the next close/fsync/reclaim — the trigger is
`open(O_RDWR); ftruncate(fd, 65536); close(fd)`.

## The reproduction (audit guest, stock kernel)

Built a 2GB hammer2 image (newfs_hammer2), wrote a 128KB file `BIGTGTNAME`
(two direct DATA blockrefs, keys 0 and 0x10000), unmounted, then `mkbadimg.c`:

* flipped the file inode's second direct blockref `type` DATA(3) → INODE(1)
  and retargeted its `data_off` radix 16→10 so the planted chain instantiates
  with HAMMER2_INODE_BYTES;
* repaired the entire integrity tree upward so the crafted media passes every
  kernel validation: xxhash64 checks of file inode → PFS-root inode →
  superroot inode → volume-header `sroot_blockset` bref, plus volume-header
  `icrc_sects[6]`/`icrc_sects[7]`/`icrc_volheader` (fixpoint loop; XXH64 seed
  `0x4d617474446c6c6e`, iscsi_crc32 — all ported/linked from the guest's
  own /usr/src).

Mount of the crafted image succeeds cleanly (`ls -l` shows the file). Then:

```
/root/trig /mnt/BIGTGTNAME 65536
open ok fd=3, ftruncate -> 65536
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
Debugger("panic")   -> db> (all CPUs stopped, guest dead)
```

(panic.txt, run.log — verbatim.)

## Exploitability ceiling

This is a denial-of-service primitive, not memory corruption: the assert fires
before any out-of-bounds access (the switch is entered with a valid locked
chain; the panic is the *defense*, not the *damage*). There is no path from
this specific site to code execution — the KKASSERT stops execution
immediately. Impact = reliable local kernel panic: any unprivileged user who
can open+truncate the planted file kills the machine; a corrupted (not
malicious) device hitting the same media state would do the same in the wild.

## Fix validation

Authored fix.diff (single change at :1582): `KKASSERT(0)` →
`error = HAMMER2_ERROR_CHECK` (mirrors the synthetic CHECK error
hammer2_chain_inode_find uses for inconsistent media, hammer2_chain.c:5720).
Cycle: `patch -p1` into the guest /usr/src, `make nativekernel
KERNCONF=X86_64_GENERIC` (BUILD_RC=0), kernel installed and rebooted
(#1 Wed Oct 14 00:21:07 UTC 2026), identical PoC re-run:

```
open ok fd=3, ftruncate -> 65536
ftruncate ok, closing (vop_close -> chain_sync xop)
SURVIVED: no panic (chain_sync did not hit KKASSERT)
TRIG_EXIT=0        (guest stays up)
```

Baseline panics, patched survives → **fix_status: fixed**
(fix_run.log, fix_build.log).

## PoC changes vs any seed

No orchestrator seed existed (pass-3 fresh finding). Development iterations:
(1) name-signature scan failed — detached inodes store the "0x%016jx" inum
string in filename[], fixed signature to detect file inodes by meta fields;
(2) single-level check repair failed mount (check tree is recursive through
indirect blocks and the volume header) → replaced with fixpoint propagation
of xxhash64 + vhdr ICRCs; (3) wle64-vs-u32 clobber of icrc_sects fixed;
(4) planted bref data_off radix retargeted 16→10 so the INODE chain
instantiates at 1024 bytes.

Guest left clean (`vm.sh reset with-src` after validation).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
