ffs_mountfs performs no validation of superblock geometry (fs_ncg/fs_ipg/fs_fpg/fs_fsize/fs_frag/fs_sbsize) β crafted image causes div-by-zero/infinite-loop/heap-overflow/bzero-corruption/OOB-read
Summary
ffs_vfsops.c:642-646 validates ONLY fs_magic and fs_bsize. ALL other geometry fields trusted directly from attacker-controlled on-disk superblock. fs_fsize==0: div-by-zero in howmany at :681 immediate mount panic. fs_frag==0: for loop :687 never advances infinite mount loop. fs_sbsize>SBSIZE(8192): :671 kmalloc(fs_sbsize) :673 bcopy(bp->b_data,ump->um_fs,fs_sbsize) reads past SBSIZE buffer = OOB read. fs_sbsize<sizeof(struct fs): field reads past kmalloc. fs_ncg==0: mount succeeds loops skipped but ffs_alloc.c:676/896 div-by-zero on first alloc = DF-0794. fs_ncg<0: :707 size=fs_ncg*sizeof(uint8) size_t wrap truncate to int=-1 :709 bzero(fs_contigdirs,-1) = heap corruption. fs_ncg huge(0x40000000): :683 size+=fs_ncg*sizeof(int32) size_t(4GiB) truncate to int kmalloc small init loops :703-704 write 4GiB past = heap overflow. fs_ipg==0/fs_fpg==0: ino_to_cg/dtog macros divide-by-zero ffs_alloc.c downstream. Root cause: ffs_mountfs never validates these. vfs.usermount=1 unprivileged trigger. Fix: validate fs_sbsize/fsize/frag/ncg/ipg/fpg/cssize/cgsize ranges after magic check + widen int size to size_t.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0820 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | full narrative: source trace, all panic signatures, escalation analysis + two hard blockers, fix, Phase-8 before/after | 12.4 KB | β raw |
| README.md | readme | human-facing summary + reproduce instructions | 3.8 KB | β raw |
| craft.c | trigger-source | superblock patcher: craft base.img out.img field=value | 4.2 KB | view raw |
| harness.c | trigger-source | transcribes L680-709 size arithmetic + write loops verbatim with poisoned allocator; characterizes each primitive | 6.7 KB | view raw |
| offsets.c | trigger-source | prints struct fs field offsets used to locate patch sites | 3.2 KB | view raw |
| reproduce.sh | trigger-source | guest-side reproducer: newfs base image, patch field, mount | 1.2 KB | view raw |
| build.sh | build-script | cc -o craft craft.c && cc -o harness harness.c | 376 B | view raw |
| run.sh | run-script | runs all variants; each panic variant crashes the guest | 1.9 KB | view raw |
| harness_output.txt | run-log | harness characterization of fsize0/ncgneg1/ncghuge/normal | 3.2 KB | view raw |
| panic_fsize0.txt | panic-signature | Fatal trap 18 integer divide fault @ ffs_mountfs+0x2ee (fs_fsize=0) | 392 B | view raw |
| panic_ncgneg1.txt | panic-signature | panic: vm_fault fault on stack guard; memset+0xd5 <- ffs_mount+0x9dc (bzero(-1)) | 1.0 KB | view raw |
| panic_ncghuge.txt | panic-signature | Fatal trap 12 page fault @ ffs_mountfs+0x58e movl %ecx,-0x4(%rsi) (maxcluster loop) | 619 B | view raw |
| fix.diff | suggested-fix | validation block (fs_sbsize/fsize/frag/ncg/ipg/fpg/cssize ranges -> EINVAL) + widen int size to size_t | 1.5 KB | view raw |
| fix_build.log | build-log | make -j6 nativekernel on the patched source, rc=0 | 5.6 MB | β download |
| fix_run.log | run-log | Phase-8: every variant EINVAL on #1 + legitimate image still mounts | 891 B | view raw |
| env.txt | environment | uname, cc version, ffs_mountfs symbol | 284 B | view 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-0820 β ffs_mountfs missing superblock-geometry validation
Status: REPRODUCED (panic / heap-corruption DoS, root-reachable). NOT exploitable
to uid=0 β two valid hard blockers (see VERDICT.md Β§3).
Impact: panic / kernel heap corruption from a crafted UFS filesystem image. The
finding's vfs.usermount=1 unprivileged-trigger claim is incorrect for UFS.
What this is
ffs_mountfs (sys/vfs/ufs/ffs_vfsops.c) validates only fs_magic and fs_bsize
(L642-646). Every other geometry field is consumed raw from the attacker-controlled
on-disk superblock β as a divisor (fs_fsize/fs_ipg/fs_fpg), an allocation size
(fs_sbsize, the int size accumulation at L680-685), and a loop bound (fs_frag
L687, fs_ncg L703). A crafted image triggers, depending on the field:
| Field | Effect | Confirmed |
|---|---|---|
fs_fsize=0 |
divide-by-zero @ L681 howmany(size,fs_fsize) |
β
Fatal trap 18 @ ffs_mountfs+0x2ee |
fs_ncg=-1 |
bzero(fs_contigdirs, (size_t)-1) @ L709 β unbounded heap zero-fill |
β
panic: vm_fault: fault on stack guard (memsetβffs_mount) |
fs_ncg=858993460 |
int size wraps small β *lp++=contigsumsize loop @ L703-704 writes 3.4 GiB of controlled value into 2 KiB |
β
Fatal trap 12 @ ffs_mountfs+0x58e (movl %ecx,-0x4(%rsi)) |
fs_frag=0 |
L687 loop never advances β infinite mount loop | β (covered by fix) |
The bzero and the controlled-value loop are genuine write primitives, but both are
unbounded synchronous stores that page-fault and panic before the mount syscall
returns, and the primitive is reachable only from a root-context mount (UFS is
not in the vfs.usermount allow-list). Both are valid Phase-6 hard blockers β no
unprivilegedβroot chain exists. See VERDICT.md for the full escalation analysis.
Files
craft.cβ superblock patcher.craft base.img out.img <field=value>...harness.cβ transcribes the L680-709 size arithmetic + write loops verbatim with a poisoned allocator; proves, per variant, exactly which line overflows and by how much.reproduce.shβ guest-side reproducer (builds base image, patches, mounts).build.sh/run.shβ exact build & run commands.offsets.cβ printsstruct fsfield offsets (used to locate patch sites).VERDICT.mdβ full narrative: source trace, all panic signatures, escalation analysis + blockers, fix, Phase-8 before/after.panic_fsize0.txt/panic_ncgneg1.txt/panic_ncghuge.txtβ crash signatures fromdfbsd-qemu/boot.log.harness_output.txtβ full harness characterization of all variants.fix.diffβ git-apply-able fix (validation block +int sizeβsize_t).fix_build.logβ full single-fix kernel build output (rc=0).fix_run.logβ Phase-8 before/after on all variants + regression.env.txtβ guest environment.
How to reproduce
The guest must be on the with-src base (default #0 GENERIC, INVARIANTS ON).
Run as root (UFS mount is not user-mountable):
# host
scp -F dfbsd-qemu/config -r findings/poc/DF-0820/. dfbsd:/root/
./dfbsd-qemu/vm.sh run_root 'cd /root && sh build.sh'
# each variant panics the guest β capture then reset between
./dfbsd-qemu/vm.sh run_root 'cd /root && sh run.sh fsize0' # Fatal trap 18
grep -iE 'fatal trap|stopped at' dfbsd-qemu/boot.log | tail
./dfbsd-qemu/vm.sh reset with-src
./dfbsd-qemu/vm.sh run_root 'cd /root && sh run.sh ncgneg1' # bzero panic
./dfbsd-qemu/vm.sh reset with-src
./dfbsd-qemu/vm.sh run_root 'cd /root && sh run.sh ncghuge' # overflow panic
Fix
Apply fix.diff to /usr/src, make -j6 nativekernel KERNCONF=X86_64_GENERIC &&
make installkernel, reboot. All crafted variants are then rejected with
EINVAL ("incorrect super block"); legitimate filesystems still mount.
Validated on single-fix kernel 6.5-DEVELOPMENT #1 (fix_run.log).
DF-0820 β ffs_mountfs missing superblock-geometry validation
Verdict: REPRODUCED (panic / heap-corruption DoS, root-reachable); NOT exploitable to uid=0 β two valid hard blockers
The underlying code bug claimed in the finding is real and confirmed: ffs_mountfs
validates only fs_magic and fs_bsize (sys/vfs/ufs/ffs_vfsops.c:642-646); every
other geometry field (fs_sbsize, fs_fsize, fs_frag, fs_ncg, fs_ipg, fs_fpg,
fs_cssize) is consumed raw from the attacker-controlled on-disk superblock as a divisor,
allocation size, and loop bound. Three distinct crash/corruption manifestations were
reproduced live on the default X86_64_GENERIC (#0, INVARIANTS ON) kernel. A targeted
fix.diff was authored, built as a single-fix #1 kernel, and validated: every crafted
variant that previously panicked is now rejected cleanly with EINVAL ("incorrect super
block") while legitimate filesystems still mount.
However, the finding's stated impact ceiling ("unprivileged trigger via
vfs.usermount=1" β LPE primitive) is incorrect, and the write primitives cannot be
converted to uid=0 on this guest. Both are documented as valid hard blockers below.
1. Source trace (every cited line confirmed against /usr/src)
ffs_mountfs (sys/vfs/ufs/ffs_vfsops.c), with the audited line numbers verified
byte-identical between the host sys/ tree and the guest /usr/src:
588: ffs_mountfs(struct vnode *devvp, struct mount *mp, struct malloc_type *mtype)
596: int error, i, blks, size, ronly; // <-- `int size`: root of the truncation
...
639: if ((error = bread(devvp, SBOFF, SBSIZE, &bp)) != 0) // reads SBSIZE=8192 bytes
641: fs = (struct fs *)bp->b_data;
642: if (fs->fs_magic != FS_MAGIC || fs->fs_bsize > MAXBSIZE ||
643: fs->fs_bsize < sizeof(struct fs)) { // <-- ONLY fields validated
644: error = EINVAL; goto out;
646: }
...
671: ump->um_fs = kmalloc((u_long)fs->fs_sbsize, M_UFSMNT, M_WAITOK); // OOB if sbsize huge
673: bcopy(bp->b_data, ump->um_fs, (uint)fs->fs_sbsize); // reads past SBSIZE buf
...
680: size = fs->fs_cssize; // int
681: blks = howmany(size, fs->fs_fsize); // DIV-BY-ZERO if fsize==0
682: if (fs->fs_contigsumsize > 0)
683: size += fs->fs_ncg * sizeof(int32_t); // size_t product, truncated by int sum
684: size += fs->fs_ncg * sizeof(uint8_t);
685: space = kmalloc((u_long)size, M_UFSMNT, M_WAITOK); // allocation uses wrapped int
...
687: for (i = 0; i < blks; i += fs->fs_frag) { // INFINITE LOOP if frag==0
...
701: if (fs->fs_contigsumsize > 0) {
702: fs->fs_maxcluster = lp = space;
703: for (i = 0; i < fs->fs_ncg; i++) // controlled-value overflow
704: *lp++ = fs->fs_contigsumsize; // when int `size` wrapped small
705: space = lp;
706: }
707: size = fs->fs_ncg * sizeof(uint8_t); // int; -1 if fs_ncg==-1
708: fs->fs_contigdirs = (uint8_t *)space;
709: bzero(fs->fs_contigdirs, size); // bzero(ptr,(size_t)-1) if ncg<0
Field offsets in struct fs (computed on-guest from sys/vfs/ufs/fs.h; sizeof(struct fs)=1384):
fs_ncg=44 fs_bsize=48 fs_fsize=52 fs_frag=56 fs_sbsize=104 fs_cssize=156 fs_cgsize=160
fs_ipg=184 fs_fpg=188 fs_contigsumsize=1316 fs_magic=1372. On-disk superblock is at
SBOFF=8192 (BBOFF+BBSIZE, fs.h:60-63).
2. Reproduced manifestations (live, on default #0 GENERIC, INVARIANTS ON)
Image-craft recipe: truncate -s 4M base.img && vnconfig -c vn0 base.img && newfs -v
/dev/vn0 && vnconfig -u vn0, then craft.c binary-patches the named 4-byte field at
SBOFF+offset. Mount via root mount -t ufs /dev/vn0 /mnt/test.
Variant C β fs_fsize == 0 (deterministic divide-by-zero)
- Path: line 681
blks = howmany(size, fs->fs_fsize)compiles toidivl %ecx,%eax. - Live:
Fatal trap 18: integer divide fault while in kernel mode/Stopped at ffs_mountfs+0x2ee: idivl %ecx,%eax(panic_fsize0.txt).
Variant A β fs_ncg == -1 (write primitive: bzero(fs_contigdirs, (size_t)-1))
- Arithmetic (verbatim, see harness_output.txt): with
fs_ncg=-1,fs_cssize=2048,fs_contigsumsize=7: - L683:
size += (-1 as size_t)*4β L684:size += (-1 as size_t)*1βint size = 2043(the size_t additions wrap and truncate to int 2043).kmalloc(2043)β small. - L703 loop: guard
i < fs_ncgβ0 < -1is false β loop SKIPPED (the finding's claim that L683 truncation drives the overflow is incorrect for negative ncg; the loop guard prevents it). No overflow from L703-704 here. - L707:
size = (-1)*1 = -1(int). L709:bzero(ptr, (size_t)(int)-1) = bzero(ptr, 0xFFFFFFFFFFFFFFFF)β unbounded zero-fill of kernel heap starting at thefs_contigdirsallocation. - Live:
panic: vm_fault: fault on stack guard, addr: 0xfffff8008dd20000with stackmemset+0xd5 β ffs_mount+0x9dc(panic_ncgneg1.txt). The fault address is a kernel stack-guard page far past the allocation, proving the bzero zeroed through many real heap objects before faulting.
Variant B β fs_ncg == 858993460 (0x33333334) (controlled-value heap overflow)
- Arithmetic (harness_output.txt):
5*ncg = 0x100000004wraps the intsizeto a small value (2048 + (5*ncg mod 2^32 as int) = 2052), sokmalloc(2052)β small. But the L703-704 loop runsfs_ncg = 858993460iterations writing the attacker-controlled valuefs_contigsumsize(=7) β 3.4 GiB of controlled int32 stores into a 2 KiB allocation. - Live:
Fatal trap 12: page fault while in kernel mode/fault virtual address = 0xfffff80118320000/Stopped at ffs_mountfs+0x58e: movl %ecx,-0x4(%rsi)(the*lp++ = fs_contigsumsizestore) (panic_ncghuge.txt).
(Variant β fs_frag == 0)
- Path: L687
for (i = 0; i < blks; i += fs->fs_frag)never advances β infinite mount loop. (Confirms finding claim; covered by the same validation block in fix.diff.)
The harness (harness.c, output in harness_output.txt) transcribes the L680-709 size
arithmetic and write loops verbatim with a poisoned allocator and proves, for each
variant, exactly which line overflows the allocation and by how many bytes.
3. Phase 6 β escalation analysis: BLOCKED (two valid hard blockers)
The bug hands the attacker two write primitives (unbounded bzero of zeros;
controlled-value int32 overflow). On this guest (no SMAP/SMEP/KASLR), a bounded
kernel write into a victim object is normally trivially convertible to uid=0. Both
primitives here are blocked from LPE conversion by valid Phase-6 blockers:
Blocker 1 β the primitive is NOT reachable from an unprivileged user (the finding's vfs.usermount=1 claim is FALSE)
get_fscap() (sys/kern/vfs_syscalls.c:5383-5398) maps only null/devfs/procfs/
tmpfs/fusefs to user-mountable capabilities; UFS falls through to
SYSCAP_RESTRICTEDROOT. In sys_mount (vfs_syscalls.c:152-159), even with
vfs.usermount=1, the check becomes caps_priv_check_td(td, RESTRICTEDROOT |
__SYSCAP_NOROOTTEST) β __SYSCAP_NOROOTTEST only bypasses the early cr_uid==0
gate; the actual capability check (caps_check_cred) still fails for a non-root
credential without the RESTRICTEDROOT cap, returning EPERM (kern_caps.c:322-352).
Verified empirically on the guest: with sysctl vfs.usermount=1, root vnconfigs the
image and chowns /dev/vn0 to maxx, then mount -t ufs as maxx β
mount_ufs: /dev/vn0: Operation not permitted. (vnconfig itself also requires
privilege.) So the crafted superblock can only be fed to ffs_mountfs by an
already-root caller. Root β kernel is not a privilege-boundary crossing (root can
already kldload an arbitrary module), so there is no unprivilegedβroot chain here.
Blocker 2 β the write is an unbounded synchronous store that page-faults and panics before the mount syscall returns
Both write paths (bzero(ptr,(size_t)-1) at L709; the *lp++ loop at L703-704) write
without any bound β they run until they hit an unmapped kernel page and trap
(proven by the live panics: vm_fault: fault on stack guard for ncgneg1; page fault
for ncghuge). The write executes synchronously in the mount syscall context; the
attacking process is blocked in the kernel the entire time and is killed by the panic
before it can observe or act on the corruption. No concurrent userspace thread can
convert the corruption into uid=0 for the attacker's own credential either: the only
credential that matters (the attacker process's) is owned by a thread stuck in mount,
and the bzero/loop races past any usable victim straight into a guard page.
Could the write be bounded to a specific victim? No: both primitive sizes are derived
from the same corrupted fs_ncg field (size = fs_ncg * sizeof(uint8_t) at L707;
the loop bound is fs->fs_ncg at L703). There is no field combination that yields a
small, controlled over-write past the allocation β every bad fs_ncg produces either a
correctly-sized (no overflow) or unbounded (page-fault) write. Hence no grooming β
victim-corrupt β convert chain exists. (On noinv the same unbounded-write β
page-fault behavior holds β the fault is from the MMU, not INVARIANTS, so the blocker
applies on both GENERIC and noinv.)
Demonstrated impact ceiling: panic / kernel heap corruption (root-reachable DoS / hardening gap). This is a real, fixable code defect β a maintainer must not accept attacker-controlled superblock geometry without validation β but it is not an LPE.
4. The fix (fix.diff)
A single targeted change to sys/vfs/ufs/ffs_vfsops.c, two hunks:
- After the existing magic/
fs_bsizecheck (L646), add a validation block returningEINVALfor:fs_sbsize < sizeof(struct fs)or> SBSIZE;fs_fsize <= 0or> fs_bsize;fs_frag <= 0or non-power-of-two;fs_ncg <= 0or> 1000000;fs_ipg <= 0;fs_fpg <= 0;fs_cssize < 0or> (1<<30). - Widen the local
int size(L596) tosize_t size, so even if a future caller bypasses the range check, the L683/L684 size arithmetic can no longer truncate.
This supersedes the finding markdown's proposal (which suggested the same fields but
did not specify the power-of-two frag check, the fs_cssize ceiling, or the
fs_sbsize >= sizeof(struct fs) lower bound needed to kill the kmalloc-undersize OOB).
5. Phase 8 β fix validation (single-fix #1 kernel)
- Baseline (
#0, unpatched): all three variants panic (see Β§2 + panic_*.txt). - Applied
fix.diffwithpatch -p1(both hunks succeeded at L593/L645), builtmake -j6 nativekernel KERNCONF=X86_64_GENERIC(rc=0, fix_build.log), installed withmake installkernel, rebooted βkern.version = 6.5-DEVELOPMENT #1: Sun Jul 5 17:02:57 UTC 2026. - Patched
#1, same crafted images: every variant now printsmount_ufs: ... : incorrect super block(EINVAL), MOUNT_RC=1, guest stays up (fix_run.log). - Regression: a freshly
newfs'd legitimate image still mounts read-write and accepts file creation on#1β the validation does not break normal operation.
Before/after:
| variant | baseline (#0) | patched (#1) |
|---|---|---|
| fs_fsize=0 | Fatal trap 18: integer divide fault @ ffs_mountfs+0x2ee | incorrect super block (EINVAL), up |
| fs_ncg=-1 | panic: vm_fault: fault on stack guard (bzero) | incorrect super block (EINVAL), up |
| fs_ncg=858993460 | Fatal trap 12: page fault @ ffs_mountfs+0x58e | incorrect super block (EINVAL), up |
| fs_frag=0 | infinite mount loop | incorrect super block (EINVAL), up |
| normal image | mounts | mounts (regression OK) |
fix_status: fixed β clean before/after on all four crash variants with no regression.
6. Caveats / next steps
- The finding's severity claim ("High, unprivileged via vfs.usermount=1") is partly wrong: the underlying validation gap is real, but the unprivileged-trigger claim is false for UFS. Realistic severity is Medium (root-context mount of attacker media β kernel panic/corruption). A genuinely unprivileged variant would exist only if an auto-mount/automount daemon (not configured on this guest) mounted attacker-supplied media as root.
- The
fs_sbsize > SBSIZEOOB read (L673 bcopy) is covered by thefs_sbsize <= SBSIZEvalidation but was not exercised live in this run (lower severity than the three confirmed panics); the harness and the validation cover it.
Fix verification
fixedVALIDATED. fix.diff applies cleanly (patch -p1, hunks at L593/L645) and builds (make -j6 nativekernel KERNCONF=X86_64_GENERIC, rc=0, fix_build.log). On the single-fix #1 kernel (kern.version 6.5-DEVELOPMENT #1 Sun Jul 5 17:02:57 UTC 2026), every crafted variant that panicked the #0 baseline is now rejected with 'mount_ufs: /dev/vn0 on /mnt/test: incorrect super block' (EINVAL), MOUNT_RC=1, guest stays up; a freshly-newfs'd legitimate image still mounts read-write and accepts file creation (regression OK). Before/after on all four variants is in fix_run.log.
fix_build.log: full nativekernel build output, ends '=== NK_DONE rc=0 === / Sun Jul 5 17:06:48 UTC 2026'. fix_run.log before/after - fsize0: #0 'Fatal trap 18 integer divide fault @ ffs_mountfs+0x2ee' -> #1 'incorrect super block' (EINVAL, up); ncgneg1: #0 'panic: vm_fault fault on stack guard' (memset<-ffs_mount, bzero(-1)) -> #1 EINVAL up; ncghuge: #0 'Fatal trap 12 page fault @ ffs_mountfs+0x58e' -> #1 EINVAL up; frag0: #0 infinite loop -> #1 EINVAL up; regression: legitimate image -> #1 MOUNT_OK.
Confirmed kernel references
- sys/vfs/ufs/ffs_vfsops.c:596
- sys/vfs/ufs/ffs_vfsops.c:642
- sys/vfs/ufs/ffs_vfsops.c:671
- sys/vfs/ufs/ffs_vfsops.c:673
- sys/vfs/ufs/ffs_vfsops.c:680
- sys/vfs/ufs/ffs_vfsops.c:681
- sys/vfs/ufs/ffs_vfsops.c:683
- sys/vfs/ufs/ffs_vfsops.c:684
- sys/vfs/ufs/ffs_vfsops.c:687
- sys/vfs/ufs/ffs_vfsops.c:703
- sys/vfs/ufs/ffs_vfsops.c:704
- sys/vfs/ufs/ffs_vfsops.c:707
- sys/vfs/ufs/ffs_vfsops.c:709
- sys/vfs/ufs/fs.h:201
- sys/vfs/ufs/fs.h:220
- sys/vfs/ufs/fs.h:284
- sys/kern/vfs_syscalls.c:152
- sys/kern/vfs_syscalls.c:5383
Detail
Exploit chain
BLOCKED - two valid hard blockers, no uid0 achievable (and no uid0 claimed). (1) Reachability blocker: the write primitives can ONLY be fed to ffs_mountfs by an already-root caller. UFS is NOT in the vfs.usermount allow-list (sys/kern/vfs_syscalls.c:5383-5398 get_fscap returns SYSCAP_RESTRICTEDROOT for ufs); even with vfs.usermount=1 the caps_priv_check (kern_caps.c:322) still requires the RESTRICTEDROOT capability, so an unprivileged user gets EPERM - verified live (mount -t ufs as maxx -> 'Operation not permitted'). vnconfig itself also requires privilege. Root->kernel is not a privilege-boundary crossing, so there is no unpriv->root chain to build. (2) Synchronous-fault blocker: both write paths are UNBOUNDED synchronous stores - bzero(ptr,(size_t)-1) at line 709 and the *lp++ loop at lines 703-704 - that run until they hit an unmapped kernel page and page-fault (proven by the live panics: vm_fault stack-guard for ncgneg1, page-fault at the store instruction for ncghuge). The write executes synchronously inside the mount syscall context; the attacking thread is blocked in the kernel the whole time and is killed by the panic before mount returns, so no userspace thread can observe/convert the heap corruption into uid0 for the attacker's own credential. Both primitive sizes are derived from the SAME corrupted fs_ncg field, so there is no field combination that yields a small, bounded over-write past the allocation suitable for grooming a specific victim object - every bad fs_ncg produces either a correctly-sized write (no overflow) or an unbounded write (page-fault). Grooming/forge/pivot techniques are therefore not applicable. This blocker is MMU-derived (not INVARIANTS-derived) and holds on both GENERIC and noinv. Demonstrated impact ceiling: panic / kernel heap corruption (root-reachable DoS). No exploit.c/chain.c was authored because no escalation chain exists on this guest - escalating would require fabricating a uid0, which I will not do.
Evidence (decisive lines)
Baseline #0 panics - fs_fsize=0: 'Fatal trap 18: integer divide fault while in kernel mode / Stopped at ffs_mountfs+0x2ee: idivl %ecx,%eax' (panic_fsize0.txt); fs_ncg=-1: 'panic: vm_fault: fault on stack guard, addr: 0xfffff8008dd20000 / memset() at memset+0xd5 / ffs_mount() at ffs_mount+0x9dc' (panic_ncgneg1.txt); fs_ncg=858993460: 'Fatal trap 12: page fault / fault virtual address = 0xfffff80118320000 / Stopped at ffs_mountfs+0x58e: movl %ecx,-0x4(%rsi)' (panic_ncghuge.txt). Harness (harness_output.txt) proves for ncg=-1: int size wraps to 2043, kmalloc(2043), then bzero(ptr,(size_t)-1) overflows by 4294965252 bytes; for ncg=858993460: int size wraps to 2052, kmalloc(2052), loop writes 3435973840 bytes of value 0x7. Patched #1 (fix_run.log): all four variants -> 'mount_ufs: ... incorrect super block' MOUNT_RC=1 guest stays up; legitimate image -> MOUNT_OK.
PoC changes
Created the DF-0820 evidence pack from scratch: craft.c (superblock field patcher using offsetof-derived on-disk offsets, handles signed/hex values, sanity-checks fs_magic); harness.c (faithful verbatim transcription of the L680-709 size arithmetic and bzero/init-loop with the int-size truncation preserved and a poisoned allocator that proves each overflow byte-for-byte, with the correct observation that the L703 loop guard i<fs_ncg makes the loop SKIP for negative ncg, so ncg<0 overflows ONLY via bzero(-1) at L709 while ncg>0 huge overflows via the L703-704 controlled-value loop); offsets.c (struct fs field-offset probe, sizeof(struct fs)=1384); reproduce.sh/run.sh/build.sh (guest-side repro flow: newfs base image via vnconfig, patch, mount as root); VERDICT.md + README.md + manifest.json.
Verified recommended fix
In sys/vfs/ufs/ffs_vfsops.c, (a) add a validation block immediately after the existing fs_magic/fs_bsize check (after line 646) returning EINVAL when fs_sbsize is outside [sizeof(struct fs), SBSIZE], fs_fsize<=0 or >fs_bsize, fs_frag<=0 or non-power-of-two, fs_ncg<=0 or >1000000, fs_ipg<=0, fs_fpg<=0, or fs_cssize<0 or >(1<<30); (b) widen the local int size at line 596 to size_t size so the L683/L684 accumulation can no longer truncate even if a future caller bypasses the range check. Full git-apply-able diff in findings/poc/DF-0820/fix.diff. SUPERSEDES the finding markdown's proposal (adds the power-of-two fs_frag check, the fs_cssize ceiling, and the fs_sbsize>=sizeof(struct fs) lower bound needed to kill the kmalloc-undersize OOB read, and explicitly specifies the size_t widening).
Verdict
REPRODUCED on default #0 GENERIC (INVARIANTS ON). The code bug claimed in DF-0820 is real and confirmed line-by-line: ffs_mountfs (sys/vfs/ufs/ffs_vfsops.c:642-646) validates ONLY fs_magic and fs_bsize; every other geometry field is consumed raw as divisor/allocation-size/loop-bound. Three distinct manifestations reproduced live on crafted UFS images: (1) fs_fsize=0 -> 'Fatal trap 18: integer divide fault' at ffs_mountfs+0x2ee (the idivl from howmany(size,fs_fsize) at line 681); (2) fs_ncg=-1 -> 'panic: vm_fault: fault on stack guard, addr 0xfffff8008dd20000' with stack memset+0xd5 <- ffs_mount+0x9dc, i.e. the bzero(fs_contigdirs, (size_t)-1) at line 709 (size_t-wrapped via int -1) zeroing kernel heap until it hits a guard page; (3) fs_ncg=858993460 -> 'Fatal trap 12: page fault' at ffs_mountfs+0x58e: movl %ecx,-0x4(%rsi), the lp++=fs_contigsumsize store in the maxcluster init loop at line 703-704 - the int size accumulation at lines 680-684 wrapped small (5ncg mod 2^32 -> kmalloc 2052) but the loop runs 858M iterations writing the attacker-controlled contigsumsize value (controlled-value heap overflow). A verbatim harness (harness.c/harness_output.txt) reproduces the size arithmetic and proves each overflow byte-for-byte. NOT exploitable to uid=0 - two valid Phase-6 hard blockers apply (see exploit_chain). The finding's stated 'unprivileged trigger via vfs.usermount=1' is FALSE for UFS: get_fscap (sys/kern/vfs_syscalls.c:5383-5398) maps only null/devfs/procfs/tmpfs/fusefs to user-mountable caps and UFS falls through to SYSCAP_RESTRICTEDROOT; empirically, with vfs.usermount=1 and /dev/vn0 chowned to maxx, mount -t ufs as maxx returns 'Operation not permitted'. So the realistic impact ceiling is panic/heap-corruption DoS reachable from a root-context mount of attacker media (a hardening gap, not LPE). fix.diff (validation block + int size -> size_t) validated on a single-fix #1 kernel: all variants cleanly rejected with EINVAL ('incorrect super block'), legitimate mounts unaffected.
No comments yet.