# DF-0804 — Panic / NULL deref via crafted HAMMER2 image in strategy_read_completion

**Severity:** Medium  **Class:** CWE-754 (Improper Check for Exceptional Conditions) / DoS
**File:** `sys/vfs/hammer2/hammer2_strategy.c:441-502`
**CVSS:** 3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

## Verdict

**REPRODUCED** on the default GENERIC kernel (`6.5-DEVELOPMENT #0`), then
**FIX-VALIDATED** on a single-fix `#1` kernel. Impact: **panic / DoS**
(kernel halt from a crafted filesystem image). No privilege escalation —
this is a pure denial-of-service at a fixed code offset.

## The bug

`hammer2_strategy_read_completion()` is the read-strategy completion callback
for HAMMER2. It dispatches on `focus->bref.type` and, for DATA blocks, on the
compression nibble of `focus->bref.methods`. Both dispatch tables terminate in
**unconditional `panic()`** instead of returning `EIO`:

* `hammer2_strategy.c:496` — `panic("unknown compression type")` for any
  `HAMMER2_DEC_COMP(methods)` value other than NONE(0)/LZ4(2)/ZLIB(3).
  `HAMMER2_COMP_AUTOZERO` (1) and crafted values 4–15 reach here.
* `hammer2_strategy.c:500` — `panic("unknown bref type")` for any
  `bref.type` other than INODE(1)/DATA(3).

Additionally the INODE branch at `:452` does an unconditional `bcopy` from
`data`; if `bref.data_off == 0`, `hammer2_chain_load_data()`
(`hammer2_chain.c:938`) returns early leaving `chain->data == NULL` and
`chain->error == 0`, so the NULL pointer reaches `bcopy` → page fault.

The `type`/`methods`/`data_off` fields are loaded **verbatim from media** by
`hammer2_chain_lookup()` / `hammer2_chain_get()` (`hammer2_chain.c:2599`) with
no value-range validation — a crafted image controls them freely.

## Threat model

Filesystem-image parsing: an unprivileged actor can supply a HAMMER2 image
(USB media, downloaded image, multi-tenant storage). When an admin (or an
automated mount) mounts it and a file is read, the kernel **panics**. This is
a local denial-of-service with no privilege boundary to cross beyond "image is
mountable."

## Reproduction (live image, full CRC re-weave)

The non-trivial part is that HAMMER2 verifies per-blockref check codes
(XXH64) and the volume header's iSCSI-CRC32 trio. A naive byte-flip of the
`methods` field fails the parent inode's CRC and the mount rejects the image.
The trigger therefore:

1. `newfs_hammer2` a fresh image, write one 64 KiB file (forces a DATA bref).
2. Flip the DATA bref `methods` low nibble: NONE(0) → 4 (undefined).
3. Walk the check cascade and recompute every covering digest so the image
   still passes integrity verification:
   * file-inode XXH64 → BOOT-inode bref `.check`
   * BOOT-inode XXH64 → SUPROOT-inode bref `.check`
   * SUPROOT-inode XXH64 → volume-header sroot bref `.check`
   * volume-header ICRC1 (sroot sector) → `icrc_sects[6]`
   * volume-header ICRC0 (sector 0) → `icrc_sects[7]`
   * volume-header ICRCVH (whole 64 KiB) → `icrc_volheader`
4. `mount_hammer2` + `cat target.bin` → on the unpatched kernel:
   `panic: hammer2_strategy_read_completion: unknown compression type`.

The re-weave is implemented in pure Python (`corrupt_image.py`) — XXH64
(validated against the reference `xxhash` package for ≥32-byte payloads) and
CRC32C/Castagnoli (validated against the `0xE3069283` test vector).

## Files

| file | purpose |
|---|---|
| `corrupt_image.py` | pure-python image corruptor (XXH64 + CRC32C, no deps) |
| `trigger.sh` | full sequence: newfs → write file → corrupt → mount → read |
| `fix.diff` | git-apply-able fix (replaces panic with EIO, adds AUTOZERO + NULL check) |
| `panic.txt` | serial-console panic signature from the unpatched run |
| `fix_run.log` | patched-kernel run output (graceful EIO, no panic) |
| `fix_build.log` | full single-fix kernel build log (rc=0) |
| `VERDICT.md` | detailed root-cause + before/after analysis |
| `env.txt` | guest environment |

## Quick reproduce

On a DragonFlyBSD master DEV guest with `hammer2` in the kernel:

```
python3 corrupt_image.py h2.img <file_off> <boot_off> <suproot_off>
vnconfig -c -S labels -T vn1 h2.img
mount_hammer2 /dev/vn1@BOOT /mnt
cat /mnt/target.bin > /dev/null      # panics on unpatched, EIO on fixed
```

(offsets come from `hammer2 show /dev/vn1 | grep inode.0`.)
