# DF-0910 — Divide-by-zero kernel panic via HAMMER_IOC_PRUNE with mod_tid=0

## Verdict
**REPRODUCED** — and **FIX VALIDATED**. The HAMMER v1 prune ioctl divides by a
user-supplied `mod_tid` with no zero-check, driving a CPU #DE (integer divide
fault) → non-resumable kernel trap → panic. Confirmed deterministically on the
unpatched `#0` kernel. The single-fix kernel (`#1`) rejects `mod_tid==0` with
`EINVAL` and does not panic.

## Mechanism (trigger → primitive → effect)

1. **Attacker input**: `HAMMER_IOC_PRUNE` ioctl with a `hammer_ioc_prune_elm`
   whose `mod_tid == 0`. The element array is copied in from userland at
   `sys/vfs/hammer/hammer_prune.c:104` (`copyin(user_elms, copy_elms,
   elm_array_size)`). The entry validation block at `:67-81` checks `nelms`,
   localization, key ordering, and `PRUNE_ALL` — but **never** validates
   `mod_tid != 0`.

2. **Reachability**: the ioctl is dispatched via `hammer_vop_ioctl`
   (`sys/vfs/hammer/hammer_vnops.c:2517`) → `hammer_ioctl`
   (`sys/vfs/hammer/hammer_ioctl.c:65`), gated by
   `caps_priv_check(cred, SYSCAP_NOVFS_IOCTL)` at `:72` — **root-only**.
   Reachability requires a HAMMER v1 mount (the guest's root is HAMMER2, so a
   HAMMER v1 image must be created and mounted; this is a realistic
   admin-precondition, not an attacker action).

3. **The divide**: during the B-tree scan, each leaf is passed to
   `prune_should_delete()` (`sys/vfs/hammer/hammer_prune.c:272`). When the
   leaf is a deleted record (`delete_tid != 0`) and falls in the element's
   `beg_tid..end_tid` range (`:302-304`), the comparison at `:305-306`
   executes:
   ```c
   (elm->base.create_tid - scan->beg_tid) / scan->mod_tid ==
   (elm->base.delete_tid - scan->beg_tid) / scan->mod_tid
   ```
   With `scan->mod_tid == 0`, this is `divq` by zero → **CPU #DE trap**.

4. **Effect**: trap 18 (#DE) from kernel mode is non-resumable → **kernel
   panic**, guest drops to DDB (`db>`), all I/O ceases. This is a **privileged
   user → kernel DoS** (Medium severity). No memory corruption, no
   escalation path — the CPU traps before any memory write occurs.

## Exploit chain / escalation
**Not applicable** — this is a divide-by-zero, not memory corruption. The #DE
trap fires before any store, so there is no write primitive, no slab
corruption, no UAF. The impact ceiling is **denial of service** (kernel panic
→ reboot). No escalation chain is derivable.

## Reproduction

**Build**: `cc -Wall -O2 -o prune_div0 prune_div0.c`

**Run** (as root, on a guest with HAMMER v1 support):
```sh
truncate -s 12G /root/df910.img
vnconfig vn0 /root/df910.img
newfs_hammer -f -L df910 /dev/vn0
mkdir -p /df910 && mount -t hammer /dev/vn0 /df910
./prune_div0 /df910   # creat+write+unlink+sync seeds a deleted record,
                      # then issues HAMMER_IOC_PRUNE with mod_tid=0
```

**On unpatched `#0` kernel** (baseline):
```
Fatal trap 18: integer divide fault while in kernel mode
cpuid = 0; lapic id = 0
instruction pointer = 0x8:0xffffffff8094bcfb
Stopped at      hammer_ioc_prune+0x31b: divq    %r9,%eax
db>
```
(The static helper `prune_should_delete` is inlined into `hammer_ioc_prune`
by gcc, hence the crash frame names the caller.) Reproduced twice
deterministically — identical panic both times.

**On patched `#1` kernel** (single-fix):
```
[!] ioctl returned rc=-1 errno=22 (Invalid argument) -- kernel NOT vulnerable
    stat_scanrecords=0 stat_rawrecords=0
```
Guest stays up. Confirmed across 3 consecutive runs.

## The fix

`fix.diff` adds a post-`copyin` validation loop that rejects any prune element
with `mod_tid == 0` (returns `EINVAL`). Placed right after the `copyin` at
`hammer_prune.c:104-105` — before `prune->elms = copy_elms` and well before
the scan loop — so no caller can reach the divide with a zero divisor.

```c
for (i = 0; i < prune->nelms; ++i) {
    if (copy_elms[i].mod_tid == 0) {
        error = EINVAL;
        goto failed;
    }
}
```

Built and validated on a single-fix kernel
(`6.5-DEVELOPMENT #1: Sun Jul 12 02:48:50 UTC 2026`): the previously-fatal
ioctl now returns `EINVAL` and the guest survives.

## PoC changes
- Wrote `prune_div0.c` from scratch (no prior PoC existed in this checkout —
  only the static-site HTML summary was present). The PoC mirrors the exact
  kernel struct layouts (`hammer_base_elm`, `hammer_ioc_head`,
  `hammer_ioc_prune_elm`, `hammer_ioc_prune`) and computes the ioctl number
  via `_IOWR('h', 1, struct hammer_ioc_prune)`.
- It seeds a deleted B-tree record (creat+write+unlink+sync) so the scan
  reaches `prune_should_delete` with a `delete_tid != 0` leaf, then issues the
  ioctl with `elms[0] = { beg_tid=0, end_tid=UINT64_MAX, mod_tid=0 }`.
- `build.sh` / `run.sh` wrap the build and the HAMMER-v1 mount setup.

## Severity / threat model
**Medium** (matches the finding). The ioctl requires `SYSCAP_NOVFS_IOCTL`
(root). A privileged user issuing a malformed prune ioctl can deterministically
panic the kernel. No unprivileged path was found: `caps_priv_check` is a hard
capability gate on the credential, not bypassable via `vfs.usermount` or
file-descriptor tricks. This is a root→kernel DoS / robustness bug.
