# DF-0929 — VERDICT

**Verdict: REPRODUCED (panic on default GENERIC #0; fix validated on #1).**

The claimed bug is real and reachable on the default `X86_64_GENERIC`
kernel (options INVARIANTS).  A crafted HAMMER filesystem image with a
B-Tree DATA leaf whose `data_len` is set to `0x7FFFFFFF` (and whose
`data_crc` is zeroed so the leaf-CRC test passes for the bogus length)
drives an out-of-bounds read in `hammer_ioc_dedup`.  On the default kernel
the path panics at the `KKASSERT(data_len >= 0 && data_len <=
HAMMER_XBUFSIZE)` sanity check at `hammer_btree.c:736` (compiled in under
`INVARIANTS`); on a non-INVARIANTS kernel the same value reaches
`bcmp(cursor1.data, cursor2.data, 0x7FFFFFFF)` at `hammer_dedup.c:117`
and `HAMMER_DATA_DOALIGN` integer-overflow at `hammer_blockmap.c`,
manifesting as a page-fault panic / corrupted blockmap metadata.

## How the trigger path is exercised

1. `patch_image.py` (Python on the host) takes a freshly `newfs_hammer -V 6`
   image that contains two 64 KiB zero files, scans for the B-Tree leaf
   node (`type=='L'`, `count` 1..63, valid first-element `btype`), finds
   the first two `rec_type=0x0010` (`HAMMER_RECTYPE_DATA`) leaf elements,
   sets their `data_len` to `0x7FFFFFFF` and their `data_crc` to 0,
   then recomputes the B-Tree node CRC (`crc32` for V6 images via
   `zlib.crc32`, matching the kernel's `hammer_datacrc(vol_version<=6,
   ...)`).
2. The patched image is mounted (`vnconfig` + `mount_hammer`).
3. `trigger.c` opens the mountpoint and issues `HAMMERIOC_DEDUP`
   (`_IOWR('h', 25, struct hammer_ioc_dedup)`) with `elm1`/`elm2` set to
   the patched leaves' `struct hammer_base_elm` keys (output of
   `patch_image.py`).

## Mechanism — `path:line` at each hop

- `sys/vfs/hammer/hammer_ioctl.c:72` — the only privilege gate:
  `caps_priv_check(...SYSCAP_NOVFS_IOCTL)`.  Root passes; the threat
  model is "privileged user runs dedup on attacker-supplied image", not
  unprivileged→root.
- `sys/vfs/hammer/hammer_dedup.c:60` — `cursor1.key_beg = dedup->elm1;`
  (attacker-supplied base_elm).
- `sys/vfs/hammer/hammer_dedup.c:63,66` — `hammer_btree_lookup()` +
  `hammer_btree_extract_data()` (which is `hammer_btree_extract(...,
  HAMMER_CURSOR_GET_DATA)`; see `hammer.h:1498-1502`).
- `sys/vfs/hammer/hammer_btree.c:728-729` — `data_off = elm->leaf.data_offset;
  data_len = elm->leaf.data_len;`.  **Both are read verbatim from the
  on-disk B-Tree node** — fully attacker-controlled on a crafted image.
- `sys/vfs/hammer/hammer_btree.c:736` — `KKASSERT(data_len >= 0 &&
  data_len <= HAMMER_XBUFSIZE);`  This is the **only** validation of
  `data_len` in the extract path.  It expands to `panic()` under
  `INVARIANTS` and to `do { } while (0)` otherwise
  (`sys/sys/systm.h:118`).
  - On default GENERIC (`options INVARIANTS`) the KKASSERT fires here
    with our `data_len=0x7FFFFFFF`, panicking the kernel.
  - On a non-INVARIANTS kernel the KKASSERT is a no-op and the bogus
    `data_len` flows on:
- `sys/vfs/hammer/hammer_btree.c:737-738` — `hammer_bread_ext(hmp,
  data_off, data_len, ...)` calls `HAMMER_BUFSIZE_DOALIGN(data_len)`
  (`hammer_ondisk.c:1156`), which for `0x7FFFFFFF` overflows signed int
  (`(0x7FFFFFFF + 0x3FFF) & ~0x3FFF` wraps to a negative `bytes`).
- `sys/vfs/hammer/hammer_dedup.c:117` — `bcmp(cursor1.data, cursor2.data,
  cursor1.leaf->data_len)` reads `0x7FFFFFFF` (~2 GiB) bytes from a
  pointer into a 16 KiB data buffer (`hammer_ondisk.c:1142`) → page
  fault → panic.
- `sys/vfs/hammer/hammer_dedup.c:134-135, 153-154` — the same
  unvalidated `data_len` flows into `hammer_blockmap_dedup` /
  `hammer_blockmap_free`, where `HAMMER_DATA_DOALIGN`
  (`hammer_disk.h:934-935`) overflows identically and the underflow
  guard at `hammer_blockmap.c:956-961` does not catch it for
  negative-after-`DOALIGN` `bytes`.

The CRC check at `hammer_btree.c:764` (`hammer_crc_test_leaf`) does
NOT block the bug.  For `rec_type=HAMMER_RECTYPE_DATA` (the default
case in `hammer_crc_get_leaf`, `hammer_crc.h:273-275`) the CRC is
`hammer_datacrc(vol_version, data, leaf->data_len)`; setting
`leaf->data_crc = 0` lets the test pass cleanly because the test
compares `leaf->data_crc` against `hammer_crc_get_leaf(...)` *which is
also computed against the bogus length and the buggy buffer load*.  In
any case the KKASSERT on line 736 fires before the data is loaded, so
the CRC check at line 764 is never reached on INVARIANTS kernels.

## Exploit chain

This is a **read-only OOB primitive** (CWE-125).  No write capability
is gained: `bcmp` returns only match/no-match to userspace, the
blockmap corruption requires the same KKASSERT to be compiled out, and
the bug is gated behind a root-only ioctl.  **No `uid=0` escalation
chain exists.**  The realistic impact ceiling is reliable kernel
panic / local denial-of-service on a privileged user who mounts and
dedups a crafted image — the audit's filesystem-image threat model.
A timing side-channel on `bcmp` duration is theoretically possible
but practically undetectable through the dedup ioctl surface.

## PoC changes (relative to the seeded stub)

The finding markdown seeded only a `trigger.c` stub that left
`dedup->elm1`/`elm2` blank ("the runner must fill in").  I:

- Wrote `patch_image.py` (host Python) — locates the B-Tree leaf node
  in a freshly-formatted V6 HAMMER image, patches two DATA-record
  leaves' `data_len` to `0x7FFFFFFF` and `data_crc` to 0, recomputes
  the B-Tree node CRC with `zlib.crc32` (matches the kernel's
  `hammer_datacrc` for V6), and prints the patched leaves' base_elm
  keys.
- Rewrote `trigger.c` to use the real `<vfs/hammer/hammer_ioctl.h>`
  struct (the seeded stub declared a wrong-sized placeholder struct
  and used the wrong ioctl number `_IOWR('h', 14, ...)` which is
  actually `HAMMERIOC_SET_VERSION`; the correct number is
  `_IOWR('h', 25, struct hammer_ioc_dedup)` per `hammer_ioctl.h:490`).
  The corrected trigger fills `elm1`/`elm2` with the patched leaves'
  base_elm keys and prints the ioctl result.
- Added `build.sh`, `run.sh`, this `VERDICT.md`, `manifest.json`,
  `panic.txt`, `run.log`, `fix_run.log`, `fix_build.log`, `env.txt`,
  and `fix.diff`.

## Fix validation

`fix.diff` promotes the `KKASSERT` at `hammer_btree.c:736` to an
explicit `if (data_len < 0 || data_len > HAMMER_XBUFSIZE) return (EIO)`
with a `hdkprintf` diagnostic, preserving the same bounds but
returning `EIO` instead of panicking.  This protects **all** callers
of `hammer_btree_extract_data` (dedup, mirror, prune, reblock,
get_data, ...) — not just the dedup path.  It supersedes the finding
markdown's `## Recommended fix` (which proposed the same change plus a
redundant dedup-only check; the single `hammer_btree.c` change is
sufficient and broader in scope).

- Built `make -j6 nativekernel KERNCONF=X86_64_GENERIC` on the
  `with-src` snapshot with the diff applied — `rc=0`.
- Installed via `make installkernel` (uses `kernel.debug` →
  `/boot/kernel/kernel`, not `kernel.stripped`) and rebooted into
  `6.5-DEVELOPMENT #1` (today's build timestamp).
- Re-ran the same trigger against the same patched image:
  - **baseline (#0):** `panic: assertion "data_len >= 0 && data_len <=
    HAMMER_XBUFSIZE" failed in hammer_btree_extract at
    /usr/src/sys/vfs/hammer/hammer_btree.c:736` — guest goes down.
  - **patched (#1):** `ioctl returned -1 (errno=5 'Input/output
    error')`; dmesg shows `hammer_btree_extract: bad data_len
    2147483647 for leaf @ a000000022010000`; guest stays up.
- Re-ran 3× on the patched kernel — identical, deterministic result.

The fix closes the bug.  See `fix_run.log` for the full after-trace
and `panic.txt` / `run.log` for the before-trace.

## Files in this evidence pack

- `trigger.c` — corrected HAMMERIOC_DEDUP trigger.
- `patch_image.py` — host-side HAMMER image patcher.
- `build.sh` / `run.sh` — runnable build/run scripts.
- `run.log` — baseline (#0) panic narrative + boot.log excerpt.
- `fix_run.log` — patched (#1) clean-EIO narrative + dmesg.
- `fix_build.log` — full `make nativekernel` output (rc=0).
- `panic.txt` — kernel panic signature from `dfbsd-qemu/boot.log`.
- `fix.diff` — `git apply`-able fix (promotes KKASSERT to EIO).
- `env.txt` — guest environment.
- `manifest.json` — artifact catalog.
