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

**Status:** REPRODUCED (DoS, #DE trap). Fix VALIDATED on a single-fix kernel.

## Verdict

REPRODUCED. The bug is real: `ffs_mountfs` (sys/vfs/ufs/ffs_vfsops.c:642-646)
only validates `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 throughout the FFS allocator. A crafted UFS1 image
with `fs_ncg=0`, `fs_cstotal.cs_nifree=1`, `fs_clean=1` mounts
read/write cleanly, then drives a fatal trap-18 (#DE) divide-by-zero the
first time the kernel tries to allocate a directory inode. The PoC
triggers it with a single `mkdir`.

This is a DoS-class bug (kernel panic), not memory corruption — there is
no escalation chain.

## Mechanism

1. Attacker supplies a 4 MB UFS1 image (`newfs -O`) whose superblock has
   been edited to `fs_ncg=0`, `fs_cstotal.cs_nifree=1`, `fs_clean=1`.
   - `fs_ncg=0` is the divisor-to-zero.
   - `cs_nifree=1` bypasses the guard at `ffs_alloc.c:596`
     (`if (fs->fs_cstotal.cs_nifree == 0) goto noinodes;`).
   - `fs_clean=1` lets the image mount read/write (without `MNT_FORCE`).
2. Admin mounts the image (or, with `vfs.usermount=1` and a chowned
   image, the unprivileged user mounts it themselves):
   `mount -t ufs /dev/vn0 /mnt`.
3. `ffs_mountfs` (`ffs_vfsops.c:642-646`) accepts the superblock because
   only `fs_magic` and `fs_bsize` are checked. The divisor fields
   (`fs_ncg`, `fs_ipg`, `fs_frag`, `fs_fpg`) are never validated.
4. Attacker creates a directory on the mounted FS: `mkdir /mnt/x`.
5. `ufs_mkdir` (`ufs_vnops.c:1278`) calls `ffs_valloc(dvp, dmode|=IFDIR,...)`.
6. `ffs_valloc` (`ffs_alloc.c:599-600`) takes the IFDIR branch and calls
   `ffs_dirpref(pip)`.
7. `ffs_dirpref` (`ffs_alloc.c:676-678`) computes
   ```
   avgifree = fs->fs_cstotal.cs_nifree / fs->fs_ncg;   /* div by 0 */
   avgbfree = fs->fs_cstotal.cs_nbfree  / fs->fs_ncg;   /* div by 0 */
   avgndir  = fs->fs_cstotal.cs_ndir    / fs->fs_ncg;   /* div by 0 */
   ```
   The CPU traps #DE (`idivl %ecx,%eax`). Because `ffs_dirpref` is
   `static` and inlined into its sole caller, the panic backtrace names
   `ffs_valloc+0x194` rather than `ffs_dirpref`.

## Evidence

Serial console (`boot.log`) on the unpatched `#0` kernel:
```
Fatal trap 18: integer divide fault while in kernel mode
cpuid = 3; lapic id = 3
instruction pointer = 0x8:0xffffffff80905894
current process      = 1045
kernel: type 18 trap, code=0
Stopped at      ffs_valloc+0x194:       idivl   %ecx,%eax
db>
```
`fs_ncg` after patch: `ncg=0`. `fs_cstotal.cs_nifree=1`. Mount output:
`mount OK; /dev/vn0 on /tmp/df0794_mnt (ufs, local)`. The trap fires on
the next `mkdir`.

## Exploit chain

None. This is a pure DoS bug (#DE / divide-by-zero). There is no memory
corruption primitive, so there is no path to `uid=0`. The realistic
impact ceiling is **local unprivileged user (or admin-mounting-attacker-
image) → kernel panic** — a permanent denial of service requiring a
reboot. The unprivileged path is available when `vfs.usermount=1` and the
admin has chowned a vnode device / image file to the user; the
privileged path is "admin mounts an attacker-supplied UFS image".

## PoC changes from initial scaffolding

No PoC scaffolding existed on disk for this finding; the runner created
the full evidence pack from scratch based on the DB summary:
- `patch_image.c` — uses `<vfs/ufs/fs.h>` `struct fs` to safely locate
  `fs_ncg`, `fs_cstotal.cs_nifree`, `fs_clean` fields and overwrite them
  in-place. Includes `<sys/types.h>` + `<sys/param.h>` (for `int32_t`,
  `MAXFRAG`) and `<vfs/ufs/ufs_types.h>` (for `ufs_daddr_t`,
  `ufs_time_t`) which `fs.h` requires.
- `build.sh` / `run.sh` — exact build/run commands.
- `fix.diff` — adds a `fs_ncg/ipg/frag <= 0` validation block to
  `ffs_mountfs` (see below).

## Fix

`fix.diff` adds a validation block in `ffs_mountfs` immediately after the
existing magic/bsize check, rejecting the mount with `EINVAL` if any of
`fs_ncg`, `fs_ipg`, or `fs_frag` is `<= 0` (all three are used as
divisors and are taken verbatim from the on-disk superblock). The block
prints a diagnostic kprintf naming the bad values. The fix is minimal
(one logical change, 13 lines) and targeted at the root cause.

This matches (and slightly broadens) the finding markdown's proposal of
"validate fs_ncg>0 in ffs_mountfs" — broadened because `fs_ipg` and
`fs_frag` are equally unvalidated divisors that the same attacker can
zero out, so validating all three at once closes the entire class for
this single fix rather than re-opening the kernel for fsck-by-fsck
follow-ups. The broader class is tracked separately as DF-0820.

## Phase 8 validation

- `vm.sh reset with-src` → unpatched `#0` baseline.
- Baseline re-confirmed: PoC panic with `Stopped at ffs_valloc+0x194: idivl %ecx,%eax`.
- `patch -p1 --forward < fix.diff` applied cleanly to `sys/vfs/ufs/ffs_vfsops.c`.
- `make -j6 nativekernel KERNCONF=X86_64_GENERIC` → `NK_DONE rc=0`.
- Installed freshly-stripped kernel to `/boot/kernel/kernel` and rebooted
  to `#1: Mon Jul 13 19:38:48 UTC 2026`,
  sha256 `9bda5319a0f577f760b48ae11528c73e502fe8b488123959f9902606604183b7`.
- Re-ran identical PoC: mount now fails with
  `mount_ufs: /dev/vn0 on /tmp/df0794_mnt: incorrect super block`
  (= the new EINVAL), dmesg shows
  `ffs_mountfs: corrupt superblock: ncg=0 ipg=512 frag=8`,
  **no #DE trap, no panic, guest stayed up**.
- Regression check: a valid UFS1 image still mounts and supports `mkdir`
  cleanly on the patched kernel.

`fix_status: fixed`.
