# DF-2606 VERDICT — reproduced

**Finding**: `sys/vfs/hammer2/hammer2_chain.c` uses on-media `bref->keybits`
(uint8_t) directly as the shift count in `(hammer2_key_t)1 << keybits` throughout
the chain lookup machinery, with no bounds validation. `hammer2_key_t` is
`uint64_t` (hammer2_disk.h:429), so keybits >= 64 is undefined behavior. The only
keybits>64 checks in the file — hammer2_chain.c:4555-4560 (freemap indirect
calculation) and its twin hammer2_chain.c:4728-4733 (directory indirect
calculation) — print `"bad bref chain"` and call `Debugger("fubar")`, then fall
through and use the value anyway (`nkeybits = bref->keybits`, then
`1 << (nkeybits - 1)` = `1 << 64`, more UB).

## How it was proven (stock kernel, 2 independent runs + 2 independent halts)

### Image construction (fully checksum-valid malicious media)

`h2keybits.c` walks the image the way the kernel does — volume header (magic +
three iCRC32s, hammer2_ondisk.c:528-558) → `sroot_blockset.blockref[0]` →
super-root inode → PFS "LOCAL" root inode — then rewrites the keybits byte of the
first INDIRECT entry in the LOCAL root's embedded 4-slot blockset
(63 → 65) and repairs every integrity check upward: XXH64(LOCAL inode) into the
sroot array entry, XXH64(sroot inode) into the volume header, then the volume
header iCRCs **in dependency order** (SECT1 → SECT0 → volheader, matching
mkfs_hammer2.c:599-611; SECT1's crc word lives inside SECT0's covered range).
Result: the kernel sees 100% valid metadata — every check it performs passes —
while slot 1's computed range collapses from [0 .. 0x7FFFFFFFFFFFFFFF] to
**[0 .. 1]** because x86-64 `SHL` masks the count mod 64 (`1<<65` → `1<<1` = 2).

### (b) Mount acceptance — no rejection anywhere

`mount_hammer2 /dev/vnX@LOCAL /mnt` succeeds (run.b.patched.log); dmesg shows the
normal mount sequence and no check failures. The kernel loads the volume header,
super-root inode, and PFS root inode without ever validating keybits of the array
entries those blocks contain.

### (a) UB arithmetic outcome — misplaced chains / lost key coverage

With keybits=65 in the LOCAL root's array:

* **readdir still lists all 25 names** — `hammer2_xop_readdir`
  (hammer2_xops.c:213-223) starts its range scan at lkey=0, which the collapsed
  [0,1] range still covers, descends into the indirect block and enumerates its
  (unmodified) children.
* **every name→inode lookup returns ENOENT** — `hammer2_xop_nresolve` looks up
  the file's 64-bit dirhash lhc; `hammer2_base_find`
  (hammer2_chain.c:4977-4993) computes `scan_end = key + ((hammer2_key_t)1 <<
  scan->keybits) - 1` = key+1, the entry no longer covers any real hash, and the
  files become unreachable: `stat /mnt/f001` → ENOENT, `cat /mnt/f013` → ENOENT,
  `ls /mnt/sub` → ENOENT — while `mount` of the pristine image (before and
  after) lists all 25 entries with identical md5s, proving the data is intact on
  media and only the kernel's key-coverage arithmetic is wrong.

The same broken math sits in the RB comparator `hammer2_chain_cmp`
(hammer2_chain.c:109-111), `hammer2_chain_find_cmp` (:1962, :2025), and the
flush/insert paths (:5307, :5357, :5366).

### The :4555 Debugger() no-op — live, twice

Creating files on the rw-mounted crafted image drives
`hammer2_chain_indkey_dir` (the check cited at :4555 has its identical twin at
:4728-4731) over the keybits=65 entry:

    bad bref chain 0 bref 0xfffff8005082e600
    Debugger("fubar")
    CPU5 stopping CPUs: 0x0000001f
     stopped
    Stopped at      Debugger+0x7c:  movb    $0,0xbdaf09(%rip)
    db>

**The whole machine halts at the DDB prompt; ssh dies; only a reset recovers it.**
Observed on two independent boots (serial-hang.log, serial-hang.2.log), from a
single `echo x > /mnt/file` (first boot) and from a 60-file create batch (second
boot; the exact trigger depends on where the new entry's fsid-salted dirhash
lands — only creates that force indirect-block topology math hit the path, a
60-name batch guarantees it). On a kernel built without DDB the same input
executes `nkeybits = bref->keybits` (65) at :4733 and `1 << 64` at :4748 —
silently poisoning the new indirect block's radix instead of halting.

## Honest ceiling (per directive: privesc — close or flag)

* **No memory-safety primitive found.** Every array walk is index-bounded by
  `count`; the UB manifests as *wrong values* (masked shift counts), never as
  out-of-bounds access. The RB comparator's "overlap counts as match"
  semantics keep the tree consistent for any comparator result. No path from
  the wrong-range math into an OOB write/read was identified: base_find/
  combined_find return bref pointers inside the parent's data; chain
  instantiation copies the bref; type dispatch still checks `bref.type`.
* Demonstrated impact: (1) filesystem namespace/logic corruption — a mounted
  crafted image makes files unreachable and directory listings lie
  (deterministic, 2/2 runs); (2) **system-halt DoS** via Debugger() from a
  single write on the rw-mounted crafted image (2/2 runs) — classified `dos`
  (machine frozen in DDB, not a crash-dump panic).
* **Privilege required: root** (`vnconfig` + `mount_hammer2`; `vfs.usermount=0`).
  No unprivileged or remote trigger exists on the tested configuration, so this
  is a malicious-media validation bug, not an escalation vector. Medium is the
  right severity; privesc bucket: **closed** (no user→root chain), flagged for
  the halt-on-write behavior which raises the practical stakes of the missing
  media validation.

## False-positive check on the cited path

The finding's original line reference (:4555, freemap variant) is real but the
*directory* twin (:4728-4731) is the variant actually reachable from ordinary
filesystem writes — both encode the same defect (detect → Debugger no-op → use
the value). All other cited shift sites verified by inspection and by the live
lookup corruption they produce.

## Fix validation (A/B on rebuilt kernel)

`fix.diff` adds keybits ≤ 63 validation at the three places on-media blockrefs
enter range math, using the existing CHECK-FAIL idiom (`krateprintf(&krate_h2chk,
...)`, `chain->error = HAMMER2_ERROR_CHECK`):

1. `hammer2_chain_load_data` — refuse to load a chain whose bref keybits > 63
   (covers brefs taken directly from the volume header, e.g. the sroot).
2. `hammer2_base_find` forward scan — skip array entries with keybits > 63
   (covers every lookup/readdir/indkey/flush scan of on-media arrays; kills the
   UB and makes handling deterministic).
3. `hammer2_base_insert` validate loop — do not compute ranges from illegal
   entries (removes the last UB and an adjacent panic corner).

Built with `make nativekernel KERNCONF=X86_64_GENERIC` (6 min, BUILD_RC=0),
installed, rebooted (#1: Tue Oct 20 02:17:42 UTC 2026):

* **Pristine image: bit-identical behavior** — same 25 entries, same md5s
  (dcd4a4a4a13297c47baaf3a5a9229c1c / d38f0794546f4e219c015a1778663d99 /
  6983b4cd210aab338877de6d3b33c926) — runfix.a.pristine.log.
* **Crafted image: cleanly handled** — mount succeeds, the corrupt subtree is
  deterministically unreachable (no UB anywhere), guest fully healthy.
* **The rw stress that froze the stock kernel twice now completes**: 60/60
  creates, sync, clean unmount, `dmesg` free of fubar/bad bref/CHECK FAIL/
  panic, guest alive — runfix.stress.log (run twice).

## Bottom line

status=reproduced, impact=dos (plus verified filesystem logic corruption),
confidence=certain. Root-only malicious-media trigger; no memory-safety or
privilege-escalation primitive exists on this path. Fix validated end-to-end.

Guest returned 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.
