# DF-0794 — Divide-by-zero panic when fs_ncg==0 — PoC evidence pack

## Summary

`ffs_mountfs` (`sys/vfs/ufs/ffs_vfsops.c:642-646`) validates only
`fs_magic`, `fs_bsize > MAXBSIZE`, and `fs_bsize >= sizeof(struct fs)`.
It never validates `fs_ncg` (number of cylinder groups), which is taken
verbatim from the on-disk superblock and then used as a divisor in the
FFS allocator. A crafted UFS1 image with `fs_ncg=0` mounts cleanly,
then triggers a fatal trap-18 (#DE) divide-by-zero on the first
directory inode allocation.

Class: **DoS (kernel panic)**. Severity: **Low** (mount-time trigger
requires either an admin mounting attacker-controlled media or
`vfs.usermount=1` + chowned image). No memory corruption → no escalation.

## How to reproduce

Prerequisites: a DragonFlyBSD guest with `newfs`, `vnconfig`, `mount`,
and `cc` (all standard). Run as root (the mount step needs root;
`vfs.usermount=1` is the unprivileged variant).

```sh
./build.sh                # cc -O2 -Wall -o patch_image patch_image.c
./run.sh                  # newfs a 4MB image, patch fs_ncg=0, mount, mkdir
```

Expected on the **unpatched** kernel (`6.5-DEVELOPMENT #0`):
- Image mounts: `mount OK; /dev/vn0 on /tmp/df0794_mnt (ufs, local)`
- `mkdir` then triggers `Fatal trap 18: integer divide fault` at
  `ffs_valloc+0x194: idivl %ecx,%eax` (inlined `ffs_dirpref`).
- Guest hangs in DDB; ssh dies. Serial console (`boot.log`) records the
  trap signature.

Expected on the **patched** kernel (`6.5-DEVELOPMENT #1`):
- Mount is rejected: `mount_ufs: incorrect super block`.
- `dmesg` shows `ffs_mountfs: corrupt superblock: ncg=0 ipg=512 frag=8`.
- No panic. Guest stays up.

## Files

- `patch_image.c` — small C helper that opens a UFS1 image, reads the
  superblock at `SBOFF` (8192), sets `fs_ncg=0`,
  `fs_cstotal.cs_nifree=1`, `fs_clean=1`, writes it back. Uses
  `<vfs/ufs/fs.h>` `struct fs`.
- `build.sh` / `run.sh` — exact build and run commands.
- `fix.diff` — `git apply`-able fix: add a `fs_ncg/ipg/frag <= 0`
  validation block in `ffs_mountfs`.
- `panic.txt` — kernel panic signature from `boot.log`.
- `run.log` — full output of the baseline (unpatched) run that panicked.
- `fix_run.log` — full output of the patched-kernel re-run that mounted-
  rejected cleanly + a regression check on a valid image.
- `fix_build.log` — full `make nativekernel` output (`NK_DONE rc=0`).
- `env.txt` — guest environment (kernels, hardening, tools).
- `VERDICT.md` — full narrative verdict.
- `manifest.json` — machine-readable artifact catalog.

## Why `ffs_valloc` and not `ffs_dirpref` in the panic?

`ffs_dirpref` is declared `static` in `ffs_alloc.c:75` and is inlined
into its sole caller `ffs_valloc` (also static). The faulting
`idivl %ecx,%eax` is the inlined `avgifree = fs->fs_cstotal.cs_nifree /
fs->fs_ncg` (ffs_alloc.c:676). The trap is real; the symbol name in the
backtrace is just the inlining artifact.

## Threat model

- Privileged path (admin trusts attacker-supplied image):
  `mount` + attacker does `mkdir` → panic. 100% reliable, no race.
- Unprivileged path (with `vfs.usermount=1` and an admin-chowned image
  or vnode device): same panic, no `wheel` membership required.
- Both are realistic; the privileged path is the more common one
  ("admin mounts attacker-controlled filesystem image").
