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)
PoC verification
Evidence pack
findings/poc/DF-0794 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 atSBOFF(8192), setsfs_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 afs_ncg/ipg/frag <= 0validation block inffs_mountfs.panic.txtβ kernel panic signature fromboot.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β fullmake nativekerneloutput (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 doesmkdirβ panic. 100% reliable, no race. - Unprivileged path (with
vfs.usermount=1and an admin-chowned image or vnode device): same panic, nowheelmembership required. - Both are realistic; the privileged path is the more common one ("admin mounts attacker-controlled filesystem image").
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
- Attacker supplies a 4 MB UFS1 image (
newfs -O) whose superblock has been edited tofs_ncg=0,fs_cstotal.cs_nifree=1,fs_clean=1. -fs_ncg=0is the divisor-to-zero. -cs_nifree=1bypasses the guard atffs_alloc.c:596(if (fs->fs_cstotal.cs_nifree == 0) goto noinodes;). -fs_clean=1lets the image mount read/write (withoutMNT_FORCE). - Admin mounts the image (or, with
vfs.usermount=1and a chowned image, the unprivileged user mounts it themselves):mount -t ufs /dev/vn0 /mnt. ffs_mountfs(ffs_vfsops.c:642-646) accepts the superblock because onlyfs_magicandfs_bsizeare checked. The divisor fields (fs_ncg,fs_ipg,fs_frag,fs_fpg) are never validated.- Attacker creates a directory on the mounted FS:
mkdir /mnt/x. ufs_mkdir(ufs_vnops.c:1278) callsffs_valloc(dvp, dmode|=IFDIR,...).ffs_valloc(ffs_alloc.c:599-600) takes the IFDIR branch and callsffs_dirpref(pip).ffs_dirpref(ffs_alloc.c:676-678) computesavgifree = 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). Becauseffs_dirprefisstaticand inlined into its sole caller, the panic backtrace namesffs_valloc+0x194rather thanffs_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#0baseline.- Baseline re-confirmed: PoC panic with
Stopped at ffs_valloc+0x194: idivl %ecx,%eax. patch -p1 --forward < fix.diffapplied cleanly tosys/vfs/ufs/ffs_vfsops.c.make -j6 nativekernel KERNCONF=X86_64_GENERICβNK_DONE rc=0.- Installed freshly-stripped kernel to
/boot/kernel/kerneland rebooted to#1: Mon Jul 13 19:38:48 UTC 2026, sha2569bda5319a0f577f760b48ae11528c73e502fe8b488123959f9902606604183b7. - Re-ran identical PoC: mount now fails with
mount_ufs: /dev/vn0 on /tmp/df0794_mnt: incorrect super block(= the new EINVAL), dmesg showsffs_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
mkdircleanly on the patched kernel.
fix_status: fixed.
Fix verification
fixedVALIDATED: 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.
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.
No comments yet.