# DF-2606 — Unvalidated bref->keybits from disk → UB shifts in hammer2 chain lookups

`sys/vfs/hammer2/hammer2_chain.c` — on-media `bref.keybits` (uint8_t, 0–255) is used
directly as the shift count in `(hammer2_key_t)1 << keybits` all over the chain code.
`hammer2_key_t` is `uint64_t`, so keybits ≥ 64 is undefined behavior; x86-64 `SHL`
masks the count mod 64 (65 → 1), silently collapsing a 2^63-byte key range into 2
bytes.  The only `keybits > 64` checks in the file (`:4555-4558` freemap,
`:4728-4731` dir) are `kprintf + Debugger("fubar")` no-ops that then *use the value
anyway* — and on the stock guest kernel (DDB compiled in) `Debugger()` **halts the
whole machine**.

Verified on the QEMU guest (DragonFly 6.5-DEVELOPMENT, stock X86_64_GENERIC,
guest /usr/src == audited tree, md5-confirmed) with a **fully checksum-valid**
crafted image: `newfs_hammer2` → populate → clean unmount → byte-patch one INDIRECT
blockref keybits 63 → 65 inside the PFS-root inode's embedded array, fixing up the
XXH64 chain checks (default check algo) and the three volume-header iCRC32s.

## Demonstrated (stock kernel)

| # | observation | evidence |
|---|---|---|
| 1 | Crafted image mounts with **no rejection**; no CRC/check errors in dmesg | run.b.patched.log, dump.patched.log |
| 2 | `readdir` still lists all 25 names, but **every name lookup returns ENOENT** (stat/cat/ls of any file or subdir) — the directory listing lies; files provably exist on media (pristine mount lists + md5s them before and after) | run.a.pristine.log, run.b.patched.log |
| 3 | One file create (or a 60-file batch) on the rw-mounted crafted image hits `hammer2_chain_indkey_dir` :4728 → `bad bref` + `Debugger("fubar")` → **all CPUs stopped, machine frozen at `db>`**, ssh dead. Reproduced on **two independent boots** | serial-hang.log, serial-hang.2.log |
| 4 | No memory corruption: the shift UB on x86-64 only masks the count; all indices stay in-bounds (base array scans are bounded by `count`). Honest ceiling: namespace/logic corruption + Debugger-halt DoS | VERDICT.md |

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

`fix.diff` validates keybits ≤ 63 at the three places on-media blockrefs enter the
range math (`hammer2_chain_load_data`, `hammer2_base_find` forward scan,
`hammer2_base_insert` validate), mirroring the CHECK-FAIL error idiom.  With the
fixed kernel: pristine image **bit-identical behavior** (same 25 entries, same
md5s — runfix.a.pristine.log); crafted image mounts and is *cleanly handled* (corrupt
subtree simply unreachable, deterministic); the rw stress that froze the stock kernel
twice now completes all creates, syncs and unmounts with **zero** kernel complaints,
guest healthy (runfix.stress.log, reproduced twice).

## Reproduce

In the DragonFly guest (as root), with this directory copied to `/root/df2606`:

    ./build.sh          # cc h2keybits.c + kernel icrc32.c + xxhash.c
    ./mkimage.sh        # newfs, populate, pristine copy, keybits 63->65 patch
    ./run.sh            # A/B/C demonstration (pristine / patched / pristine)
    ./stress.sh         # WARNING: freezes the stock kernel at db> (DoS proof)

Expected on stock kernel: A lists 25 + md5s; B mounts with no rejection, lists 25
names, every lookup ENOENT; C lists 25 + md5s again.  stress.sh halts the machine
("bad bref chain ..." + Debugger("fubar") on the serial console).

## Attack preconditions (reachability)

Attacker must be **root** on the box (or have equivalent rights to `vnconfig` +
`mount_hammer2` a crafted image): `vfs.usermount=0` and no unprivileged path exists
to feed hammer2 a crafted image on this configuration.  A remote/unprivileged trigger
does not exist; the bug class is malicious-media handling.  Severity stays Medium
(fail-closed media validation missing); impact ceiling is filesystem logic corruption
plus a Debugger()-halt DoS — no memory-safety primitive found (see VERDICT.md).

## Files

    h2keybits.c        walker/patcher (dumps topology, patches keybits, fixes all checks)
    build.sh mkimage.sh run.sh stress.sh stress-fix.sh   exact runnable commands
    mkfs.log patch.log dump.pristine.log dump.patched.log  image construction
    run.a.pristine.log run.b.patched.log                  stock-kernel A/B (run 1, captured)
    serial-hang.log serial-hang.2.log                    stock-kernel DDB halts (2 boots)
    runfix.a.pristine.log runfix.b.patched.log runfix.stress.log   fixed-kernel A/B/stress
    fix.diff           git-apply-able fix (validated: nativekernel build + install)
    env.txt VERDICT.md manifest.json verdict.json
