# DF-0875 — hammer2_get_volume unconditional panic on out-of-range data_off

## Verdict: REPRODUCED (panic / kernel DoS via crafted image); FIX VALIDATED

**Severity:** Medium (CVSS 3.1: `AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H` — local DoS
via a crafted filesystem image that an admin or auto-mounter mounts).

---

## The bug

`hammer2_get_volume()` in `sys/vfs/hammer2/hammer2_ondisk.c` unconditionally
calls `panic()` when a blockref's `data_off` does not fall within any mounted
volume's offset range:

```c
// sys/vfs/hammer2/hammer2_ondisk.c:731-732
if (!ret)
    panic("no volume for offset 0x%016jx", (intmax_t)offset);
```

`ret` is only set if the linear scan over `hmp->volumes[]` finds a volume whose
`[offset, offset+size)` range contains the queried offset. If none matches (an
out-of-range `data_off`), `ret` stays NULL and the kernel panics.

### Reachability (mount path, traced line-by-line)

1. A crafted HAMMER2 image has a volume header with valid magic + all three
   CRC32C sectors (ICRC0/ICRC1/ICRCVH) correct, but `sroot_blockset.blockref[0].data_off`
   set to an offset outside every volume (e.g. `0x004000000000000e` — 1 PB, radix 14).
2. On `mount -t hammer2`, the kernel reads the volume header and validates the
   CRCs (`hammer2_read_volume_header`, `hammer2_ondisk.c:528-558`). All pass.
3. Mount sets up `hmp->vchain` with `bref.type = HAMMER2_BREF_TYPE_VOLUME` and
   `vchain.data = &hmp->voldata` (`hammer2_vfsops.c:1189-1192`).
4. Mount locates the super-root inode: `hammer2_chain_lookup()` on key
   `HAMMER2_SROOT_KEY` starting from `vchain` (`hammer2_vfsops.c:1273-1276`).
5. The lookup finds `voldata.sroot_blockset.blockref[0]` (the corrupted entry,
   `hammer2_chain.c:1249-1252`) and tries to load its on-disk data.
6. `hammer2_chain_load_data()` calls `hammer2_io_bread()` with the corrupted
   `data_off` (`hammer2_chain.c:998-1000`).
7. `_hammer2_io_bread()` → `_hammer2_io_getblk()` → `hammer2_io_alloc()`
   (`hammer2_io.c:201`).
8. `hammer2_io_alloc()` computes `pbase = lbase & pmask`. The corrupted offset
   is non-zero and page-aligned, so the `KKASSERT(pbase != 0 && ...)` at
   `hammer2_io.c:126` **passes**.
9. `hammer2_io_alloc()` calls `hammer2_get_volume(hmp, pbase)`
   (`hammer2_io.c:142`). No volume matches → **`panic("no volume for offset ...")`**
   at `hammer2_ondisk.c:732`.

The `KKASSERT` at `hammer2_io.c:126` only catches `pbase == 0` or a
cross-page-boundary block; any non-zero, page-aligned, out-of-range offset
passes straight through to the unconditional panic.

### Impact

**Kernel panic / denial of service.** A local user who can supply a crafted
HAMMER2 filesystem image (USB media, downloaded disk image, forensic analysis,
auto-mount) and have it mounted (root, or `vfs.usermount=1`) triggers an
immediate kernel panic. This is a DoS, **not** a memory-corruption / write
primitive — no escalation chain is possible (the panic is an immediate halt,
not a corruptible write). Phase 6 (escalation) does not apply.

---

## Reproduction

**Trigger PoC** (`patch_image.c` + `verify_image.c` + `run.sh`):

1. Create a 256 MB image, `vnconfig`, `newfs_hammer2 -L testfs`.
2. Patch all present volume-header copies: set
   `sroot_blockset.blockref[0].data_off` to `0x004000000000000e`
   (offset 1 PB, radix 14 = 16 KB block).
3. Recompute the three CRC32C values (ICRC1 sector-1 → ICRC0 sector-0 →
   ICRCVH whole-block, in dependency order) so the kernel accepts the header.
4. `mount -t hammer2 /dev/vn0@testfs /mnt` → **panic**.

The CRC32C implementation in `patch_image.c` / `verify_image.c` uses the
standard Castagnoli polynomial (reflected `0x82F63B78`, init `0xFFFFFFFF`,
final XOR) matching the kernel's `iscsi_crc32()` (`sys/libkern/icrc32.c:777`).
It was validated against a known-good `newfs_hammer2` image before use —
all three CRCs matched on the original (uncorrupted) header.

### Panic signature (decisive evidence — from `boot.log`)

```
panic: no volume for offset 0x0040000000000000
hammer2_get_volume() at hammer2_get_volume+0x4b 0xffffffff8098726b
_hammer2_io_getblk() at _hammer2_io_getblk+0x477 0xffffffff809650f7
_hammer2_io_bread() at _hammer2_io_bread+0x17 0xffffffff80965397
hammer2_chain_load_data() at hammer2_chain_load_data+0x2a5 0xffffffff8096f4a5
hammer2_chain_lock() at hammer2_chain_lock+0xde 0xffffffff8096fa4e
```

---

## Fix

`fix.diff` (git-apply-able, validated on a built+booted single-fix kernel).
**Supersedes** the finding markdown's one-line proposal ("return NULL, caller
checks") by adding the full NULL-propagation chain the caller graph requires:

1. **`hammer2_ondisk.c:731`** — `hammer2_get_volume()`: replace `panic()` with
   a diagnostic `kprintf` + `return NULL`; drop the now-redundant
   `KKASSERT(ret)`.
2. **`hammer2_io.c:142`** — `hammer2_io_alloc()`: if `hammer2_get_volume()`
   returns NULL, return NULL instead of creating a dio with a NULL
   `vol->dev->devvp`.
3. **`hammer2_io.c:205`** — `_hammer2_io_getblk()`: in the `createit` path,
   if `hammer2_io_alloc()` returns NULL, return NULL.
4. **`hammer2_io.c:399`** — `_hammer2_io_putblk()`: guard against `*diop == NULL`
   (the error-cleanup path in `hammer2_chain_load_data` calls
   `hammer2_io_bqrelse(&chain->dio)` with a NULL dio; without this guard the
   fix's first iteration NULL-dereferenced here — `Fatal trap 12`).
5. **`hammer2_io.c:579,589,612`** — `hammer2_io_new()`, `hammer2_io_newnz()`,
   `_hammer2_io_bread()`: if `*diop == NULL`, return `EIO` instead of
   dereferencing `(*diop)->error`.

The error then propagates through the existing clean path:
`hammer2_chain_load_data()` sees `error != 0` → sets `chain->error =
HAMMER2_ERROR_EIO` → `hammer2_mount()` sees `schain->error` → returns `EINVAL`
(`hammer2_vfsops.c:1285-1294`).

### Fix validation (Phase 8)

- **Baseline (unpatched `#0`)**: PoC panics — `panic: no volume for offset
  0x0040000000000000`, guest dead in DDB. (`fix_baseline_reproduced = true`)
- **Patched (`#1`, sha256 `87ad15e5...`)**: PoC returns `mount: Invalid argument`
  (EINVAL), guest stays up. Dmesg shows the clean diagnostic chain:
  `hammer2_get_volume: no volume for offset ...` → `hammer2_chain_load_data:
  I/O error ...: 5` → `hammer2_mount: error I/O Error reading super-root`.
  (`fix_patched_reproduced = false` = bug does NOT reproduce)
- **Determinism**: ran twice, both clean EINVAL, no panic.
- **Regression check**: an uncorrupted `newfs_hammer2` image still mounts and
  is writable on the patched kernel (`MOUNT_RC=0`, `WRITE_OK`). No regression.

---

## PoC changes

Authored from scratch (the finding had no pre-existing PoC folder):
- `patch_image.c` — CRC32C-correct image patcher (corrupts `data_off`,
  recomputes ICRC0/ICRC1/ICRCVH in dependency order).
- `verify_image.c` — validates all three volume-header CRCs (used to confirm
  the CRC implementation matches the kernel before mounting).
- `build.sh` / `run.sh` — exact reproducible build and run commands.
- `fix.diff` — the validated fix (supersedes the finding's proposal).
