# DF-0818 — Integer divide-by-zero in HAMMER2 bulkfree_pass progress (latent)

## Verdict
**NOT REPRODUCED** (latent / defense-in-depth). The unguarded divide at
`sys/vfs/hammer2/hammer2_bulkfree.c:727` is a real source-level foot-gun, but
the claimed trigger path (`volu_size = 0` from a crafted image) is closed by an
upstream guard. The bug is unreachable through any current mount path.

## What the finding claimed
`hammer2_bulkfree.c:727` computes `incr = bfi->sstop / (hmp->total_size / 10000)`
with no guard against `total_size < 10000`. If `total_size = 0`, the integer
divisor is 0 → #DE trap → kernel panic. The finding asserts that a crafted
HAMMER2 image with `volu_size = 0` can pass alignment checks and reach the
bulkfree path via the (unprivileged) `HAMMER2IOC_BULKFREE_SCAN` ioctl.

## What the source actually does — full trace

`hmp->total_size` is only ever assigned at three places:
- `sys/vfs/hammer2/hammer2_vfsops.c:1229` — `hmp->total_size = hmp->voldata.total_size;` (v2 multi-volume)
- `sys/vfs/hammer2/hammer2_vfsops.c:1232` — `hmp->total_size = hmp->voldata.volu_size;` (v1 single-volume)
- `sys/vfs/hammer2/hammer2_ioctl.c:1394` — `hmp->total_size += delta;` (growfs; only increases)

So at the moment `hammer2_bulkfree_pass` could be entered, `hmp->total_size`
equals the on-disk `volu_size` (or its sum across volumes, plus any growfs
delta — strictly non-decreasing). The reachability question reduces to:
**what values may `voldata.volu_size` legally take after a successful mount?**

`voldata.volu_size` is read from the volume header in `hammer2_read_volume_header`
(`sys/vfs/hammer2/hammer2_ondisk.c:485`) — only after the CRC triplet (sect0,
sect1, volheader) and magic have validated. Then `vol->size = voldata->volu_size`
at `hammer2_ondisk.c:684`. After all volumes are loaded, `hammer2_verify_volumes`
calls `hammer2_verify_volumes_common` (the gate that runs for both v1 and v2):

```c
/* sys/vfs/hammer2/hammer2_ondisk.c:292 */
if (vol->size == 0) {
    hprintf("%s has size of 0\n", path);
    return EINVAL;
}
```

This **rejects `volu_size == 0`** (since `vol->size == volu_size`). The finding's
claim that "volu_size=0 passes alignment check (0&MASK==0)" is true for the
alignment check at line 352 in isolation but is moot: the earlier `vol->size == 0`
check at line 292 fires first.

The version-specific verifiers additionally require:
- **v1** (`hammer2_verify_volumes_1`, `hammer2_ondisk.c:352`):
  `vol->size & HAMMER2_VOLUME_ALIGNMASK64` must be 0. The smallest non-zero
  aligned value is `HAMMER2_VOLUME_ALIGN = 8 MiB = 0x800000 = 8,388,608`
  (`hammer2_disk.h:261`).
- **v2** (`hammer2_verify_volumes_2`, `hammer2_ondisk.c:441` & `:453`):
  non-last volumes must be ≥ `HAMMER2_FREEMAP_LEVEL1_SIZE` (1 GiB), last volume
  must be aligned to 8 MiB.

So the smallest mountable `total_size` on any version is **8 MiB**, which makes
the line-727 divisor `8388608 / 10000 = 838` (non-zero). And `total_size == 0`
is rejected at line 292. **No value of `total_size < 10000` can survive mount
validation** in the current code.

## Experimental confirmation

`craft_zero.py` produces a CRC-valid HAMMER2 image with `volu_size = 0`
(starting from a real `newfs_hammer2` image, then recomputing the sect0/sect1/
volheader CRC32-C values). Mount attempts against it produce, in dmesg:

```
hammer2_ondisk: "/dev/vn0" zone=0 id=0 offset=0x0000000000000000 size=0x0000000000000000
hammer2_ondisk: /dev/vn0 has size of 0                <-- line 292 rejection
```

For a `volu_size = 8192` (sub-8MiB, sub-10000) image, mount fails the v2
`total_size != sum of volumes` and/or the alignment check; never reaches
`hammer2_bulkfree_pass`.

The unprivileged ioctl path itself **is** reachable (this is the separate
issue tracked as DF-0815 — `HAMMER2IOC_BULKFREE_{SCAN,ASYNC}` are deliberately
exempt from the `caps_priv_check` gate at `hammer2_ioctl.c:83`, see
`hammer2_ioctl.c:144-149`). The PoC `poc.c`, run as `maxx` against a
chowned-to-user HAMMER2 mount, successfully invokes the ioctl and the
bulkfree code runs through line 727 — for a valid 128 MiB image the divisor
is `13421`, no panic, `incr=10000`, exit 0:

```
BULKFREE_SCAN rc=0 errno=0 (Undefined error: 0)
  sstop=134217728 ...
```

So the **path** is real; the **trigger condition** (`total_size < 10000`) is
not.

## Why this is still worth a fix.diff (defense-in-depth)

The divisor at line 727 lacks an explicit zero-guard. Today it is protected
by upstream volume validation, but that is an *implicit* invariant — easily
broken by any future change that loosens the size validation, by a future
caller that bypasses mount, or by the (currently `#undef`'d) in-kernel
remaster path at `hammer2_bulkfree.c:504`. A one-line guard converts a latent
#DE panic into safe behavior. The fix follows the finding's proposed shape:

```c
if (hmp->total_size < 10000)
    incr = 10000;
else
    incr = bfi->sstop / (hmp->total_size / 10000);
```

## Exploit chain
none — not memory corruption, latent div0 unreachable from any current
user-controlled input.

## PoC changes
`findings/poc/DF-0818/` did not exist when this run began. Authored:
- `poc.c` — unprivileged `HAMMER2IOC_BULKFREE_SCAN` ioctl exerciser (proves the
  path is reachable, but does not panic for any mountable image).
- `craft_zero.py` — forges a CRC-valid HAMMER2 image with `volu_size = 0`
  (or any chosen value) to demonstrate the upstream rejection at
  `hammer2_ondisk.c:292`.
- `fix.diff` — defense-in-depth zero-guard (supersedes the finding's proposal;
  same shape, with explanatory comment).

## Fix validation
Built and booted a single-fix kernel (`6.5-DEVELOPMENT #1`) carrying the
zero-guard. The PoC against a valid 128 MiB HAMMER2 image behaves identically
on the unpatched baseline and the patched kernel (rc=0, `sstop=134217728`,
bulkfree completes "100.00% storage processed", no panic). This is expected
because the bug was never reachable in the first place; the patch is
defense-in-depth only.
