# DF-0870 PoC — Unprivileged HAMMER volume_list disclosure

## Claim (from the finding)

> An unprivileged user can leak kernel heap bytes via the HAMMER
> `HAMMERIOC_LIST_VOLUMES` ioctl because (a) the LIST_VOLUMES case in
> `hammer_ioctl.c:213` has no per-case `caps_priv_check` (unlike
> ADD_VOLUME at line 197 / DEL_VOLUME at line 207), and (b) root can
> `volume-add` with a non-NUL-terminated `device_name` so `kstrdup`
> walks past the buffer and the leaked heap bytes persist as
> `volume->vol_name`, recoverable later by any user via LIST_VOLUMES.

## What this PoC actually demonstrates

* **The bypass at `hammer_ioctl.c:213-216` IS real and unprivileged.**
  Run as `maxx` (uid 1001, no wheel) on a file on a HAMMER mount,
  `HAMMERIOC_LIST_VOLUMES` returns `rc=0` and discloses each volume's
  `vol_no` and `device_name` (the backing block-device path).
  `HAMMERIOC_ADD_VOLUME` from the same caller correctly returns `EPERM`.

* **The "kernel heap info leak" half does NOT reproduce.**  The kstrdup
  OOB read is real as a code observation, but it cannot persist:
  - unprivileged users cannot reach `hammer_ioc_volume_add` (gated by the
    top-level caps check via `if (error == 0)` at line 196);
  - even when reached by root, `hammer_install_volume()` then calls
    `nlookup()` on the captured (1024+N)-byte garbage string, which fails
    with `ENAMETOOLONG`, so `hammer_free_volume()` kfree's the leaky
    `vol_name` before it is ever inserted into the mount's volume list;
  - the subsequent `LIST_VOLUMES` still returns the original clean
    device path.

So the user-observable leak is the volume device path (e.g. `/dev/vn0`),
which is normal filesystem metadata — not uninitialized kernel heap bytes.

See `VERDICT.md` for the full mechanism walkthrough with `path:line`
citations.

## Setup (one-time, requires root)

The audit guest boots HAMMER2 on `/`, so a HAMMER1 filesystem must be
created for this test.  Run on the guest as root:

```sh
# 12 GB sparse image is the newfs_hammer minimum (>10 GB) without -f tricks.
dd if=/dev/zero of=/var/tmp/hammer.img bs=1m count=12000
vnconfig vn0 /var/tmp/hammer.img
newfs_hammer -f -L TESTHAMMER /dev/vn0
mkdir -p /mnt/hammer
mount_hammer /dev/vn0 /mnt/hammer
touch /mnt/hammer/testfile
chown maxx:maxx /mnt/hammer/testfile
chmod 666 /mnt/hammer/testfile
```

## Build

```sh
./build.sh        # cc -o poc poc.c -I/usr/src/sys ; cc -o poc_root poc_root.c -I/usr/src/sys
```

## Run (as the unprivileged user)

```sh
ssh dfbsd-maxx   # or otherwise become uid 1001
cd /path/to/poc/DF-0870
./run.sh /mnt/hammer/testfile
```

### Expected on the unpatched `#0` kernel

```
HAMMERIOC_LIST_VOLUMES: rc=0 errno=0 (Success) nvols=1
  [0] vol_no=0 device_name='/dev/vn0' len=8
HAMMERIOC_ADD_VOLUME (non-NUL device_name): rc=-1 errno=1 (Operation not permitted)
```

### Expected on the patched `#1` kernel (after `fix.diff`)

```
HAMMERIOC_LIST_VOLUMES: rc=-1 errno=1 (Operation not permitted)
HAMMERIOC_ADD_VOLUME (non-NUL device_name): rc=-1 errno=1 (Operation not permitted)
```

## Run the privileged probe (proves the heap-leak half doesn't persist)

```sh
ssh dfbsd
/tmp/poc_root /mnt/hammer/testfile
```

Expected:

```
HAMMERIOC_ADD_VOLUME (non-NUL device_name): rc=-1 errno=63 (File name too long)
As expected: install failed -- kstrdup-captured leaked name is not a valid path; vol_name freed.
LIST_VOLUMES after failed ADD: rc=0 errno=0 nvols=1
  [0] vol_no=0 device_name='/dev/vn0' len=8
```

## Fix

`fix.diff` adds the missing `if (error == 0)` gate before the
`hammer_ioc_volume_list()` call (matching every privileged sibling case),
which closes the demonstrable unprivileged disclosure.  The
defense-in-depth NUL-termination/`strnlen` hardening on the `device_name`
field remains worthwhile but addresses a non-user-observable path.
