# DF-0797 — Unvalidated `vol_no` from crafted HAMMER volume header → kernel heap OOB write in `volume_map` bitmap

## Verdict
**REPRODUCED** — immediate kernel panic on GENERIC `#0` from a crafted HAMMER
image; deterministic OOB-write primitive proven by harness; **fix VALIDATED**
on a single-fix kernel (#1) — the bad vol_no is rejected with EFTYPE/EINVAL
before the OOB write, no panic.

## Mechanism (trigger → primitive → effect)

A HAMMER v1 volume header carries `int32_t vol_no` at byte offset `0x90` in
`struct hammer_volume_ondisk` (`sys/vfs/hammer/hammer_disk.h:754`). At mount:

1. `hammer_install_volume()` reads the header into a buffer and at
   **`sys/vfs/hammer/hammer_ondisk.c:210`** does
   `volume->vol_no = ondisk->vol_no;` — a raw int32 trusted from disk with
   **NO range check**.
2. At **`:234`** it calls `hammer_volume_number_add(hmp, volume)`.
3. `hammer_volume_number_add` (`sys/vfs/hammer/hammer.h:1579-1584`) computes
   `int i = __hammer_vol_index(vol->vol_no)` and writes
   `hmp->volume_map[i] |= __hammer_vol_low(vol->vol_no)`.
4. `__hammer_vol_index` (`sys/vfs/hammer/hammer.h:1567-1571`) is just
   `vol_no >> 6` with **NO mask**. `hmp->volume_map[]` is `uint64_t[4]`
   (`sys/vfs/hammer/hammer.h:875`, the LAST field of `struct hammer_mount`),
   so valid indices are 0..3 (covers vol_no 0..255 = `HAMMER_MAX_VOLUMES`).

For a crafted `vol_no` outside `[0, 256)`:

| `vol_no`       | `__hammer_vol_index` | effect of `volume_map[i] |= bit`                       |
|---------------|----------------------|--------------------------------------------------------|
| `0`           | `0`                  | in-bounds (legitimate root volume)                     |
| `256`         | `4`                  | 8-byte OOB OR-write past `volume_map[3]`               |
| `256+64*K+N`  | `4+K`                | OOB OR-write of bit `N` at byte `(K*8 + N/8)` past end |
| `0x7FFFFFFF`  | `0x1FFFFFF`          | ~255 MiB past `volume_map` → unmapped page → **panic** |
| `-1`          | `-1` (`0x3FFFFFF`)   | ~512 MiB past → unmapped page → **panic**              |

The `HAMMER_VOL_ENCODE`/`HAMMER_VOL_DECODE` macros (`hammer_disk.h:291-294`)
mask to 8 bits for offset encoding; `__hammer_vol_index` does NOT — this
asymmetry is the root cause.

The rootvol sanity check at `hammer_ondisk.c:241` runs AFTER the OOB write at
`:234`, so it cannot prevent it.

### CRC is NOT validated at mount
`hammer_crc_test_volume()` (`sys/vfs/hammer/hammer_crc.h:180`) is defined but
has **ZERO callers** anywhere in `sys/vfs/hammer/` (verified by `grep`).
Therefore patching `vol_no` in the image requires NO CRC recompute — the
volume header CRC is never checked at mount time. (A separate hardening gap.)

## Reproduction

**Trigger** (acceptable precondition: admin mounts a crafted filesystem image):

1. `dd if=/dev/zero` a 1 GB image; `vnconfig -c vn0 img; newfs_hammer -f -L TEST /dev/vn0`.
2. Mount, seed a file, sync, unmount, `vnconfig -u`.
3. `./craft_img img 2147483647` — patches `vol_no` at offset 0x90 to `0x7FFFFFFF`.
4. `vnconfig -c vn0 img; mount -t hammer -o nohistory /dev/vn0 /mnt` → panic.

**Captured panic** (GENERIC `#0`, `with-src` baseline, from `dfbsd-qemu/boot.log`):

```
Fatal trap 12: page fault while in kernel mode
cpuid = 2; lapic id = 2
fault virtual address	= 0xfffff80128a50a28
fault code		= supervisor write data, page not present
instruction pointer	= 0x8:0xffffffff80948789
current process		= 979
kernel: type 12 trap, code=2
Stopped at      hammer_install_volume+0x519:    orq     %rdi,0x10a30(%rbx,%rdx,8)
db>
```

The faulting instruction `orq %rdi,0x10a30(%rbx,%rdx,8)` is the EXACT
compilation of `hmp->volume_map[i] |= bit`:
- `%rbx` = `hmp`; `0x10a30` = offset of `volume_map[]` in `struct hammer_mount`
- `%rdx` = `i` = `0x1FFFFFF`; `%rdx,8` = `i*8`
- `%rdi` = the bit (1<<63) to OR

So the kernel page-faulted executing `volume_map[0x1FFFFFF] |= bit` —
**definitive in-kernel proof of the DF-0797 OOB write** at `hammer.h:1583`
called from `hammer_ondisk.c:234`.

### Deterministic OOB proof (`./harness`)
The harness transcribes `__hammer_vol_index`/`__hammer_vol_low`/`volume_map[] |=`
verbatim against a poisoned heap and shows the OOB extent for vol_no = 0, 256,
0x200, 0x7FFFFFFF, -1. It proves the controllable-offset primitive:
`vol_no = 256 + 64*K + N` → single-bit OR-write of bit `N` at qword `(4+K)`,
i.e. byte offset `(K*8 + N/8)` past `volume_map` end. Both qword offset and
bit-within-qword are attacker-controlled.

## Impact ceiling

- **GENERIC (INVARIANTS ON, the default):** immediate, deterministic kernel
  panic / DoS at mount time via the far-OOB variant (`vol_no=0x7FFFFFFF`).
  The small-OOB variant (`vol_no=256..319`) silently ORs a bit into adjacent
  kernel heap past `struct hammer_mount` — content of the write is a single
  set bit at a controllable offset, value not fully attacker-shaped (only
  OR-set, not arbitrary). On GENERIC, no INVARIANTS guard fires on this
  specific write (it is a plain array index, not a slab-redzone violation),
  so the small-OOB case corrupts silently.
- **Production (INVARIANTS OFF):** the controllable-offset single-bit OR is a
  constrained heap-write primitive, but escalation is bounded by:
  (a) the write is OR-only (can set a bit, not clear or write arbitrary bytes),
  (b) it requires a victim object with a security-interesting single-bit
      flip to land exactly at the controllable offset,
  (c) the target offset is relative to the end of `struct hammer_mount`, a
      large (>64 KiB) allocation, so the reachable victim window is whatever
      is allocated immediately after it — not easily groomable to a specific
      victim.
  No escalation chain was developed: the primitive (single-bit OR at a
  controllable but hammer_mount-relative offset, triggered only at mount of a
  crafted image by root) is too narrow to convert to `uid=0` on this guest.
  The realistic ceiling is therefore **panic / DoS on GENERIC** (reliable,
  mount-time) and a **latent constrained heap-write primitive** on production.

**Threat model / preconditions:** the bug requires mounting a crafted HAMMER
filesystem image. `vfs.usermount=0` on this guest, so the mount itself is a
root-only action (acceptable: "admin mounted a crafted filesystem image");
the panic is the demonstration of impact. There is no unprivileged-only path
to the OOB — the corruption happens DURING `mount`, before any unprivileged
file operation.

## PoC changes (what was built from scratch)
- `craft_img.c` — volume-header `vol_no` patcher. Locates the HAMMER_FSBUF_VOLUME
  signature at block 0, patches the 4-byte `vol_no` field at offset `0x90`,
  leaves `vol_crc` stale (verified: CRC is never validated at mount). No
  CRC32C machinery needed (unlike DF-0769/0776).
- `harness.c` — deterministic transcription of `__hammer_vol_index` /
  `volume_map[] |=` showing the OOB extent for several `vol_no` values against
  a poisoned heap model.
- `build.sh` / `run.sh` — exact build & run pipeline (image create + forge +
  mount, plus harness).

## Fix (`fix.diff` — git-apply-able, validated)
Two-part, minimal:

1. **`hammer_ondisk.c`** (before `:234` `hammer_volume_number_add`): reject
   out-of-range `vol_no` with `EFTYPE`:
   ```c
   if (volume->vol_no < 0 || volume->vol_no >= HAMMER_MAX_VOLUMES) {
       hkprintf("volume %s has invalid vol_no %d\n", volume->vol_name, volume->vol_no);
       error = EFTYPE;
       goto late_failure;
   }
   ```
2. **`hammer.h` `__hammer_vol_index`** (defense in depth): mask the index to
   the array size: `return ((vol_no >> 6) & 0x3);`

This **matches** (and slightly broadens, with the inline mask as defense in
depth) the finding markdown's `## Recommended fix` proposal.

## Fix validation (Phase 8)
- **Baseline `#0`** (`with-src`, unpatched): crafted image mount →
  `Fatal trap 12 ... Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8)`
  → guest wedged in DDB. ✓ bug present.
- **Single-fix kernel `#1`** (built with `make -j6 nativekernel KERNCONF=X86_64_GENERIC`,
  rc=0, sha256 `3b1974ce583a3d22c5f67ee1e07999b1b7ddff8f6f3067b636c55d15d0af6d16`):
  same crafted image mount → `mount: Invalid argument` (rc=1),
  dmesg shows `HAMMER: volume /dev/vn0 has invalid vol_no 2147483647`,
  **no panic, guest stays up, `kern.version=#1`**. ✓ fix closes the bug.
- The same `vol_no < 0 || vol_no >= HAMMER_MAX_VOLUMES` guard also rejects
  the small-OOB variant (`vol_no=256`, 256>=256) and the negative variant
  (`vol_no=-1`, <0) by code inspection.

Build: `make -j6 nativekernel` (HAMMER is in X86_64_GENERIC); ~6 min for a
`.c`+`.h` change; only `hammer_ondisk.c` + `hammer.h` recompiled + link.
