# DF-2618 — Overlapping key ranges in on-disk blockref arrays unvalidated — chain-insert collision panic / rbtree root wipe

* Verified 2026-08-28 by df-bsd-reviewer (verify mode) on the QEMU/KVM
  DragonFly guest, stock INVARIANTS kernel #0 (Thu Jul 2 06:02:54 2026) and
  fix-validated on rebuilt kernel #1. *

## What the finding claims
`hammer2_chain_cmp()` (sys/vfs/hammer2/hammer2_chain.c:97-118) treats
overlapping `[key, key + 2^keybits - 1]` ranges as a match (cmp == 0).  A
crafted on-disk parent whose blockref array contains two overlapping brefs
drives `RB_INSERT` in `hammer2_chain_insert()` (chain.c:313-320) into a
collision: INVARIANTS builds panic at the KASSERT (chain.c:314); release
builds continue with a *phantom* chain (ONRBTREE set but never linked) whose
later `RB_REMOVE` wipes `parent->core.rbtree`'s root (sys/sys/tree.h:641-652).

## Result
**REPRODUCED (panic), then FIXED.**

* Baseline (stock INVARIANTS kernel): mounting the forged image and `stat`ing
  a file whose inum-keyed lookup crosses the overlapping entries panics
  deterministically:

      panic: hammer2_chain_insert: collision 0xfffff80118c22100 0xfffff80118c21980 (key=0000000000000401)

      hammer2_chain_insert() at hammer2_chain_insert+0x15b
      hammer2_chain_get()    at hammer2_chain_get+0x64
      hammer2_chain_lookup() at hammer2_chain_lookup+0x615
      hammer2_chain_inode_find() at hammer2_chain_inode_find+0x117
      hammer2_xop_nresolve()    at hammer2_xop_nresolve+0x1a5

  (`panic.txt`, serial capture; guest down.)  The trigger is pure
  namei/readdir activity on a mounted image — no special privileges beyond
  the mount itself.

* Release-build consequence (rbtree root wipe): **verified by line-precise
  source trace** — see VERDICT.md §3.  Not demoed on a no-INVARIANTS kernel
  (build cost); the trace is exact and the INVARIANTS panic proves the
  primitive.

* Fix validation (`fix.diff`, rebuilt kernel #1): the same crafted image
  mounts, `ls` lists every dirent, lookups that cross the corrupt entries
  fail with ENOENT instead of panicking, the fix's rate-limited
  `hammer2_chain_insert: collision ... (key=0000000000000401)` message
  appears exactly once on the console, a **write** to the corrupt directory
  succeeds, umount is clean, and the system (which roots on hammer2) stays
  up.  Control image: 100% normal behavior on both kernels.

## Reproduce

Host side (from this directory):

    ./build.sh     # mkbase2618.sh in guest -> base2618.img; forge -> craft2618.img; push
    ./run.sh       # control run (no panic), then craft run (panic on stock kernel)

Guest side:

    sh /root/poc/df2618/trigger.sh control   # pristine image: everything works
    sh /root/poc/df2618/trigger.sh craft     # forged image: panic at `stat c`
                                            # (serial: vm.sh log; see panic.txt)

For fix validation: apply fix.diff in the guest /usr/src, `make -j6
nativekernel && make installkernel`, reboot, rerun both triggers
(fix_control_run.log / fix_run.log / fix_write_smoke.log).

## The forged image

`forge_df2618.py` walks volhdr -> sroot -> PFS "testvol" inode -> indirect
array and patches two file-INODE brefs so their key ranges overlap:

    INODE(a) key=1024 keybits 0->1   => [1024,1025]   (swallows 1025)
    INODE(b) key=1025 keybits 0->1   => [1025,1026]   (extends beyond)

Sorted array order is preserved (only keybits changed).  CHECK_NONE
(methods=0x00) is set on the covering indirect bref, the PFS inode bref and
the sroot bref, and all volhdr CRC32Cs are recomputed (technique proven in
DF-2616/DF-2617/DF-2620).

Trigger mechanics (see VERDICT.md for the full trace): `stat a` pins
chain(1024)=[1024,1025] in the indirect chain's core.rbtree via the inode
cache; `stat c` -> `hammer2_chain_inode_find(1026)` -> `base_find` skips
[1024,1025] (its end < 1026) and stops at the overlapping [1025,1026] bref;
`hammer2_chain_find` misses (pinned chain ends at 1025); `chain_get` ->
`RB_INSERT` -> `cmp == 0` -> KASSERT panic.
