# DF-0869 — Verdict

**Verdict: REPRODUCED (panic / local DoS via root-only ioctl) — fix VALIDATED**

## Summary

The finding is real. `hammer_format_volume_header` in
`sys/vfs/hammer/hammer_volume.c:617-671` only validates the result of its
geometry computation with a single signed-`<0` check on `vol_buf_size`
(line 664). The three attacker-controlled int64 inputs to that
computation — `ioc->boot_area_size`, `ioc->memory_log_size`, and
`ioc->vol_size` — are never sanity-checked, so a hand-crafted
`HAMMERIOC_ADD_VOLUME` ioctl produces an on-disk volume header whose
`vol_buf_beg` is un-sector-aligned (and/or whose `vol_buf_size` exceeds
the zone-2 addressable range). The kernel then panics in
`hammer_format_freemap` while servicing the same ioctl.

`HAMMERIOC_ADD_VOLUME` is gated behind
`caps_priv_check(cred, SYSCAP_NOVFS_IOCTL)` at
`sys/vfs/hammer/hammer_ioctl.c:197` — i.e. root only. This is therefore
a root-only kernel-panic / local DoS, exactly matching the filed
severity (Low) and CVSS `PR:H / A:L`. It is **not** an unprivileged
escalation.

## Mechanism (trigger → primitive → effect)

Trigger: `HAMMERIOC_ADD_VOLUME` issued against a mounted HAMMER
filesystem with:
- `boot_area_size  = -1`
- `memory_log_size = 0`
- `vol_size        = 2^49` (~512 TiB)
- `device_name     = /dev/vn2`  (a vnconfig'd 1 GiB sparse file)

Code path (cited to `sys/`):

1. `hammer_ioctl.c:197` — caps_priv_check passes (root).
2. `hammer_volume.c:103` — `hammer_format_volume_header(hmp, ioc, &ondisk, free_vol_no)`:
   - `vol_alloc = root_ondisk->vol_bot_beg;`                       // 0x40000
   - `vol_alloc += ioc->boot_area_size;`                           // -1
   - `vol_alloc += ioc->memory_log_size;`                          // 0
   - `ondisk->vol_buf_beg = vol_alloc;`                            // **0x3FFFF = 262143 (NOT 512-aligned)**
   - `ondisk->vol_buf_end = ioc->vol_size & ~(int64_t)HAMMER_BUFMASK;`  // 0x2000000000000
   - The only guard: `if (HAMMER_VOL_BUF_SIZE(ondisk) < 0) return EFTYPE;`
     `vol_buf_size = 0x1FFFFFFFC0001` is **positive**, so the guard
     **passes** and the bogus header is committed.
3. `hammer_volume.c:107` — `hammer_install_volume(hmp, ioc->device_name, NULL, &ondisk)`
   succeeds (it accepts the bogus geometry without further checks);
   `volume->maxbuf_off` is set from the corrupt `vol_buf_end`.
4. `hammer_volume.c:117` — `hammer_format_freemap(trans, volume)` is called.
   Inside it (`hammer_volume.c:406-407`):
   ```c
   vol_buf_size = HAMMER_VOL_BUF_SIZE(ondisk);    // 0x1FFFFFFFC0001
   KKASSERT((vol_buf_size & ~HAMMER_OFF_SHORT_MASK) == 0);
   ```
   Because `HAMMER_OFF_SHORT_MASK = 0x000FFFFFFFFFFFFF` is **52 bits**
   (not 48 as the finding markdown supposed), `vol_buf_size ≈ 2^49`
   *passes* this KKASSERT. (My first trace assumed 48 bits and expected
   a panic here; the live kernel proved otherwise — see the simulator
   output referenced in `panic.txt`.)
5. `hammer_format_freemap`'s first layer-2 `hammer_bread()` translates
   the encoded zone-2 offset for `vol_no=1` into a byte offset on vn2
   equal to `vol_buf_beg = 262143`. The vn driver rejects the I/O:
   ```
   dscheck(vn2): bio_offset 262143 is not on a sector boundary (ssize 512)
   ```
   `hammer_bread` returns `EINVAL`.
6. `hammer_volume.c:118` — `KKASSERT(error == 0);` fires after
   `hammer_format_freemap` returns the error:
   ```
   panic: assertion "error == 0" failed in hammer_ioc_volume_add
          at /usr/src/sys/vfs/hammer/hammer_volume.c:118
   ```

Trace (from `panic.txt`, captured from `dfbsd-qemu/boot.log`):
```
hammer_ioc_volume_add() at hammer_ioc_volume_add+0x5c6
hammer_ioctl()           at hammer_ioctl+0xf0a
hammer_vop_ioctl()       at hammer_vop_ioctl+0x48
vop_ioctl()              at vop_ioctl+0x63
vn_ioctl()               at vn_ioctl+0xb1
Debugger("panic")
```

So the bug is real and the kernel panics on the default `#0` GENERIC
kernel (INVARIANTS on). The exact KKASSERT that fires is `error == 0`
at line 118 (not the `(vol_buf_size & ~HAMMER_OFF_SHORT_MASK) == 0`
assertion at line 407), but the **root cause is identical**: missing
bounds validation in `hammer_format_volume_header` lets an un-aligned /
out-of-range `vol_buf_beg` propagate into `hammer_format_freemap`'s
I/O. Either KKASSERT is a panic — both are reachable from the same
unchecked inputs.

## Privilege / threat model

`HAMMERIOC_ADD_VOLUME` requires `caps_priv_check(cred,
SYSCAP_NOVFS_IOCTL)` (root). The bug is a root-only kernel-panic DoS,
not an unprivileged escalation. Realistic impact:

- A HAMMER admin (or a buggy / compromised userspace HAMMER tool) can
  panic the kernel by issuing a single malformed ADD_VOLUME ioctl.
- On a non-INVARIANTS kernel the KKASSERTs are compiled out, and
  `hammer_format_freemap` would instead run its 4 TiB-stride loop over
  the bogus `aligned_vol_free_end`, producing a sustained I/O storm /
  kernel livelock (also DoS). We did not separately reproduce this on
  `noinv`; the panic on the default GENERIC kernel is the more
  important and more realistic demonstration.

## PoC

- `trigger.c` — opens the mountpoint and issues the crafted ioctl.
- `run.sh` — sets up the base HAMMER fs (newfs_hammer), mounts it,
  creates a 1 GiB vn-backed file for the new volume, then runs the
  trigger. Run as root.

### Build & run

```
./build.sh            # cc -o trigger trigger.c
ssh dfbsd
cd /root/df0869_staged/DF-0869 && ./run.sh
```

### Expected output

- **Unpatched `#0` kernel**: trigger never returns; the guest dies
  mid-syscall. `boot.log` shows:
  ```
  HAMMER(df0869) Initialize freemap volume 1
  dscheck(vn2): bio_offset 262143 is not on a sector boundary (ssize 512)
  panic: assertion "error == 0" failed in hammer_ioc_volume_add
         at /usr/src/sys/vfs/hammer/hammer_volume.c:118
  ```
- **Patched `#1` kernel**: trigger returns promptly
  `ioctl returned -1 (errno=79 'Inappropriate file type or format')`;
  guest stays up. `dmesg` shows:
  ```
  HAMMER(df0869) volume 1 has non-positive geometry
  HAMMER(df0869) An error occurred: 79
  ```

## PoC changes from initial draft

There was no initial PoC (`findings/poc/DF-0869/` did not exist). The
runner authored the entire evidence pack from scratch: `trigger.c`,
`run.sh`, `build.sh`, `README.md`, `panic.txt`, `env.txt`,
`fix.diff`, plus the run/build logs and `manifest.json`. Two iterative
fixes were required during development:

1. The new-volume file path was originally passed as a regular file
   path; `hammer_install_volume` requires a vnode disk via `vn_isdisk`,
   so `run.sh` was updated to `vnconfig -c vn2` the new-volume file
   and pass `/dev/vn2`.
2. A 0-byte newvol file was reported as `unused` by `diskinfo` and
   rejected; `run.sh` was updated to `truncate -s 1G` the newvol file
   first. (This is fine for the threat model: the bug is that
   `ioc->vol_size` is never validated against the *real* device size,
   so a 1 GiB file happily "advertises" 2^49 bytes of buffer area.)

A small C simulator (`/tmp/sim.c`) was used to compute `vol_buf_size`
exactly and resolve why the line-407 KKASSERT did not fire (the macro
masks 52 bits, not 48 as the finding markdown supposed).

## Recommended fix (`fix.diff`)

Two-layer validation in `hammer_format_volume_header`, both returning
`EFTYPE` (the existing error used by this function):

1. **Input validation** — reject `boot_area_size < 0`,
   `memory_log_size < 0`, or `vol_size <= 0` up front. These fields
   reserve on-disk space; negative values are nonsensical and are
   what produced the un-aligned `vol_buf_beg` in this PoC.
2. **Result validation** — after the existing `<0` check, also require
   `HAMMER_VOL_BUF_SIZE(ondisk) <= HAMMER_OFF_SHORT_MASK` (i.e. the
   buffer area fits in the zone-2 short-offset range, satisfying the
   `KKASSERT` at `hammer_volume.c:407`) **and** that `vol_buf_beg` is
   `HAMMER_BUFSIZE`-aligned (avoids the sector-boundary panic at the
   `hammer_bread` site).

The fix is minimal and targeted; it does **not** change the function
signature, the success path, or any legitimate add-volume operation.
A `hammer volume-add` of a real 1 GiB vn device to a 2 GiB HAMMER fs
succeeds on the patched kernel (negative-regression test passed).

The full standalone git-apply-able diff is in `fix.diff`. It
**supersedes** the finding markdown's `## Recommended fix` proposal
(none was filed); the runner authored it post-verification with
line-accurate root-cause understanding.

## Phase 8 — fix validation

- **Baseline (unpatched `#0`)**: PoC panics the kernel mid-syscall;
  guest dies; panic signature in `boot.log` names
  `hammer_ioc_volume_add` at `hammer_volume.c:118`. (`run.log` +
  `boot.log.unpatched_panic` + `panic.txt`.)
- **Patched (`#1`, today's build, sha256
  `2ffacc98086014898d71ea0e421c21ceed0d2341f7b8c0ce590dbeac81af9556`)**:
  PoC returns cleanly with `errno=79 (EFTYPE)`; guest stays up across
  3 consecutive runs; `dmesg` shows
  `HAMMER(df0869) volume 1 has non-positive geometry`. A legitimate
  `hammer volume-add /dev/vn2 /mnt/df0869` succeeds on the patched
  kernel (`volume-list` shows both vn1 and vn2), proving the fix is
  non-breaking.

=> `fix_status: fixed` (clean before/after contrast).
