β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0794

Divide-by-zero panic when fs_ncg==0 β€” unvalidated superblock geometry mounts successfully then traps on first allocation

Summary

ffs_mountfs (ffs_vfsops.c:642-646) validates only fs_magic fs_bsize<=MAXBSIZE fs_bsize>=sizeof(struct fs). fs_ncg NEVER checked. Crafted UFS1 image fs_ncg=0 fs_cstotal.cs_nifree=1 fs_clean=1 mounts successfully. ffs_dirpref :676-678 avgifree=avgbfree=avgndir = ... / fs->fs_ncg = div by zero #DE trap panic. ffs_hashalloc :896 cg=(icg+2)%fs->fs_ncg = mod by zero panic. ffs_blkpref :819-820 startcg%=fs->fs_ncg avgbfree/=fs->fs_ncg same. ffs_valloc guard :596 if(cs_nifree==0) does NOT catch attacker sets nifree nonzero. Trigger: crafted UFS image mount then mkdir/creat immediate panic 100% reliable no race. Fix: validate fs_ncg>0 in ffs_mountfs.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0794 Β· 13 files
FileTypeDescriptionSize
patch_image.c trigger-source patches a real newfs UFS1 image's superblock: fs_ncg=0, fs_cstotal.cs_nifree=1, fs_clean=1 2.8 KB view raw
build.sh build-script cc -O2 -Wall -o patch_image patch_image.c 309 B view raw
run.sh run-script newfs image, patch, vnconfig+mount, mkdir -> div-by-zero 1.8 KB view raw
fix.diff suggested-fix validate fs_ncg/ipg/frag > 0 in ffs_mountfs 852 B view raw
run.log run-log baseline unpatched-#0 run: mount OK then ssh died; serial console shows fatal trap 18 in ffs_valloc 2.2 KB view raw
fix_run.log fix-log patched-#1 run: mount rejected 'incorrect super block', no panic; regression check on valid image OK 1.7 KB view raw
fix_build.log build-log full make -j6 nativekernel output, NK_DONE rc=0 5.6 MB ↓ download
panic.txt panic-signature Fatal trap 18 integer divide fault; Stopped at ffs_valloc+0x194: idivl %ecx,%eax 243 B view raw
env.txt environment guest kernels (#0 baseline + #1 fix), hardening state, tools 1.6 KB view raw
VERDICT.md verdict full narrative: mechanism, evidence, fix, Phase 8 validation 5.5 KB ↓ raw
README.md readme human-facing summary + reproduce instructions 3.3 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing summary + reproduce instructions
↓ download raw

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).

./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").
VERDICT.md verdict full narrative: mechanism, evidence, fix, Phase 8 validation
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline #DE panic at ffs_valloc+0x194; patched EINVAL 'incorrect super block', guest up. Valid-image regression OK.

BEFORE: Fatal trap 18 idivl at ffs_valloc+0x194. AFTER: EINVAL mount rejected, guest up. Regression: valid image mounts + mkdir OK.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Jul 13 19:38:48 UTC 2026 (sha256 9bda5319...)

Confirmed kernel references

Detail

Exploit chain

none (DoS / #DE divide-by-zero trap; no memory-corruption primitive).

Evidence (decisive lines)

BASELINE: Fatal trap 18 at ffs_valloc+0x194: idivl %ecx,%eax, guest down. PATCHED: mount rejected with EINVAL 'incorrect super block', dmesg 'ncg=0', guest up. Valid-image regression: mount+mkdir OK.

PoC changes

Authored from scratch: patch_image.c (sets fs_ncg=0/cs_nifree=1/fs_clean=1 at SBOFF), run.sh, fix.diff (reject fs_ncg/ipg/frag <=0 in ffs_mountfs), build.sh, VERDICT.md, manifest.json.

Verified recommended fix

Add divisor validation block in ffs_mountfs after magic/bsize check: reject fs_ncg<=0 || fs_ipg<=0 || fs_frag<=0 with EINVAL. Full git-apply-able diff in findings/poc/DF-0794/fix.diff.

Verdict

REPRODUCED. ffs_mountfs validates only fs_magic and fs_bsize, never fs_ncg. Crafted UFS1 image with fs_ncg=0 mounts cleanly, then ffs_valloc (ffs_alloc.c:676 avgifree=cs_nifree/fs_ncg) divides by zero -> Fatal trap 18 #DE. Root-only mount, attacker-controlled image.