ffs_reload (MNT_RELOAD) adopts the re-read superblock's geometry without validating it against the mount-time allocations β heap OOB write with attacker-controlled length and content, divide-by-zero, and heap OOB read disclosed to the media
Summary
ffs_reload() re-reads the superblock from the backing device and validates only fs_magic/fs_bsize (:484-488); bcopy(newfs, fs, old_sbsize) (:500) then adopts every other geometry field of the attacker-influenced re-read sb while fs_csp/fs_maxcluster/fs_contigdirs still point into the allocation sized for the OLD geometry at mount time (:671/:685). The comment at :493 ('important parameters (eg fs_ncg) are unchanged') is an unchecked assumption. Consequences: step-3 csum re-read bcopy's howmany(new_cssize,new_fsize) frags of on-disk bytes into the old fs_csp allocation (heap OOB write, length=adopted cssize, content=on-media bytes); the maxcluster refill writes new_ncg int32s past the array (:533-537); howmany() divides by adopted fs_fsize (:515); a later RW upgrade runs ffs_sbupdate's bcopy(fs, bp, adopted fs_sbsize) past the mount-time um_fs (:1292-1295) writing adjacent kernel heap to the raw media; mnt_maxsymlinklen re-imported (:504 - DF-3015's trust boundary). Root mounts attacker-supplied media RO; the sb on media differs from what the mount consumed (trivial for crafted media); an admin/tool issues the documented post-fsck reload (mount(2) MNT_UPDATE|MNT_RELOAD|MNT_RDONLY) -> kernel heap corruption with fully controlled bytes, kernel panic, or kernel-heap disclosure onto attacker-readable media. UFS mounts root-gated on stock configs (empirically proven; vfs.usermount does not help - DF-0820 Blocker 1) - root-context corruption primitive, not an unpriv->root LPE; an automount daemon would make it unpriv-triggerable. The cssize variant is strictly stronger than DF-0820's primitives: a silently-completing, content-and-length-controlled overwrite (groomable to any victim adjacent to fs_csp). GUEST-VERIFIED 6 variants: (a) cssize 2048->0x80000 + pre-patterned csaddr -> RELOAD RETURNED 0 having bcopy'd 524288 pattern bytes into the 2053-byte fs_csp chunk (~522KB silent heap smash, guest kept running); (b) fsize=0 -> Fatal trap 18 idivl; (c) ncg 1->0x20000000 -> stack-guard vm_fault at the *lp++ store; (d) sbsize->65536 + RW upgrade -> Fatal trap 12 in memmove (sbupdate OOB read); (e) sbsize->8192 + RW upgrade -> 6144 bytes beyond um_fs written to media (dd-readable); (f) adoption proven via crafted fs_dsize surfacing in df. uid=0 chain: none - trigger root-gated (documented blocker). Fix validated (geometry-equality gate + fs_fmod=0): all crafted variants -> clean EINVAL, unchanged reloads and RW mounts unaffected.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3078 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.5 KB | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| reload.c | β | 667 B | view raw | |
| craft.c | β | 821 B | view raw | |
| dumpsb.c | β | 966 B | view raw | |
| setup.sh | β | 335 B | view raw | |
| build.sh | β | 177 B | view raw | |
| run.sh | β | 1.9 KB | view raw | |
| build.log | β | 962 B | view raw | |
| run.log | β | 3.2 KB | view raw | |
| run.patched.log | β | 1.8 KB | view raw | |
| panic_idivl_fsize0.txt | β | 841 B | view raw | |
| panic_maxcluster_ncg.txt | β | 1.0 KB | view raw | |
| panic_sbupdate_oob_read.txt | β | 655 B | view raw | |
| env.txt | β | 714 B | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| manifest.json | β | 1.8 KB | view raw | |
| verdict.json | β | 4.9 KB | view raw |
DF-3078 β ffs_reload (MNT_RELOAD) adopts re-read superblock geometry without validation
What
ffs_reload() (sys/vfs/ufs/ffs_vfsops.c:446-548) is the UFS MNT_RELOAD
handler (reachable via mount(2) with MNT_UPDATE|MNT_RELOAD|MNT_RDONLY on a
read-only-mounted filesystem β the classic "fsck just fixed the backing media,
re-read everything" path). It:
- re-reads the superblock from the device (
bread(devvp, SBOFF, SBSIZE), :479); - validates only
fs_magicandfs_bsizebounds (:484-488) β the same two fieldsffs_mountfschecks (DF-0820); bcopy(newfs, fs, old_fs_sbsize)(:500) β adopts every other geometry field of the re-read superblock (fs_cssize,fs_fsize,fs_csaddr,fs_frag,fs_ncg,fs_sbsize,fs_ipg,fs_inopb, shifts,fs_fmod,fs_maxsymlinklen, β¦) whilefs->fs_csp(:495/:516), thefs_maxclusterarray andfs_contigdirsstill point into the mount-time allocation sized for the OLD geometry (ffs_mountfs:680-709:cssize + ncg*4 + ncg*1bytes, 2053 for a default small fs).
The comment at :493 claims "important parameters (eg fs_ncg) are unchanged" β nothing enforces that; the superblock on the backing device is attacker data whenever the media can change between mount and reload.
Primitives (all verified on the guest, stock #0 INVARIANTS kernel)
| variant | site | primitive |
|---|---|---|
fs_cssize 2048β0x80000 |
step-3 csum loop :515-529 bcopy(bp->b_data, space, size) |
heap OOB write, length + content fully disk-controlled (512KB of pattern bytes into the 2053-byte fs_csp chunk); run completed silently (RELOAD RETURNED), guest kept running with ~510KB of heap smashed |
fs_ncg 1β0x20000000 |
maxcluster refill :533-537 *lp++ = fs->fs_contigsumsize |
heap OOB write, 2GB of controlled-value stores β panic: vm_fault: fault on stack guard at ffs_mount+0xab9 (panic_maxcluster_ncg.txt) |
fs_fsize β0 |
blks = howmany(fs->fs_cssize, fs->fs_fsize) :515 |
divide-by-zero: Fatal trap 18 ... idivl %ecx,%eax at ffs_mount+0x627 (panic_idivl_fsize0.txt) |
fs_sbsize 2048β65536 + RW upgrade |
ffs_sbupdate :1292-1295 bcopy(fs, bp->b_data, fs->fs_sbsize) |
heap OOB read of 63KB past the mount-time um_fs allocation β Fatal trap 12, supervisor read in memmove (panic_sbupdate_oob_read.txt) |
fs_sbsize 2048β8192 + RW upgrade |
same | silent variant: 6144 bytes beyond the in-core superblock written to the media at SBOFF β a kernel-heap disclosure oracle readable with dd (leak region was zero pages on the sparse test guest; on a busy system it is live heap) |
Adoption of the re-read sb in a returning reload is proven independently:
patching fs_dsize (sb+40) makes df /mnt/rt report the new value
(31108 blocks / -342% avail) immediately after mount(RELOAD).
Also re-imported by the same bcopy: mp->mnt_maxsymlinklen = fs->fs_maxsymlinklen
(:504) β the DF-3015 symlink-overflow trust boundary gets a second import
path, and ump->um_nindir/um_bptrtodb/um_seqinc (:729-731, mount-time only)
desynchronize from the new fs_nindir/fs_fsbtodb/fs_frag.
Reachability / threat model
Mounting UFS and issuing MNT_RELOAD both require root on this system
(get_fscap() maps ufs to SYSCAP_RESTRICTEDROOT β same blocker as DF-0820;
verified there empirically). Realistic threat: root mounts attacker-supplied
media (USB stick, VM disk image, downloaded .img), the media content changes
(or is different from what was sanity-glanced), root or an admin tool issues
mount -u reload β kernel heap corruption / panic / heap disclosure.
Not an unprivβroot LPE on the standard configuration; an automount daemon
mounting media as root would make it unprivileged-triggerable.
Files
reload.cβ the trigger:mount(2)MNT_UPDATE|MNT_RELOAD|MNT_RDONLY (note: DF'smount_ufshas no-o reloadsupport, so the syscall must be called directly)craft.c/dumpsb.cβ superblock patcher / dumper (SBOFF=8192)run.sh <variant>β full repro driver (see header)panic_*.txtβ the three panic signaturesfix.diffβ geometry cross-check in ffs_reload (+ forcefs_fmod=0); validated: all crafted variants β cleanEINVAL, unchanged reload works, fresh mounts work
Build / run (host side)
dfbsd-qemu/vm.sh reset with-src # or: up b=$(base64 -w0 setup.sh) ... # push setup.sh/craft.c/dumpsb.c/reload.c vm.sh run_root 'cd /root/rt && sh build.sh && sh run.sh cssize'
Guest
DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC (INVARIANTS on), x86_64.
DF-3078 β VERDICT
REPRODUCED β heap out-of-bounds write (controlled content and length),
divide-by-zero, and heap out-of-bounds read (media-disclosure oracle), all via
ffs_reload adopting a re-read superblock without validating its geometry
against the mount-time allocations. Root-reachable (mount + MNT_RELOAD),
panic/corruption impact; not an unprivβroot LPE on the standard
configuration (hard blocker: UFS mounts are root-gated, same as DF-0820).
Root cause (path:line, verified in-tree and on-guest)
ffs_reload (sys/vfs/ufs/ffs_vfsops.c:446):
- :479-488 β re-reads the sb; validates ONLY
fs_magic+fs_bsizebounds. - :493-495 β comment asserts "important parameters (eg fs_ncg) are
unchanged"; nothing checks it. The mount-time allocations that depend on the
OLD geometry:
um_fs = kmalloc(fs_sbsize)(:671),fs_csp = kmalloc(cssize + ncg*[contigsum] + ncg)(:680-685). - :500 β
bcopy(newfs, fs, (uint)fs->fs_sbsize)adopts every field of the re-read sb (the old sbsize bounds the copy, so the copy itself is safe β the adopted values are the bug). - :515 β
blks = howmany(fs->fs_cssize, fs->fs_fsize)β div-by-zero on adoptedfs_fsize==0. Reproduced: Fatal trap 18, idivl, ffs_mount+0x627. - :515-529 β step-3 loop re-reads
howmany(NEW_cssize, NEW_fsize)frags ofNEW_csaddrinto the OLDfs_csp:bcopy(bp->b_data, space, size)walksspaceforward β heap OOB write, length = adopted cssize, content = on-media bytes (fully attacker-chosen on crafted media). Reproduced (cssize 2048β0x80000): 524288 bytes copied into the 2053-byte allocation β reload RETURNED 0, guest kept running with ~522 KB of heap smashed by the on-disk pattern. (Notably this variant is a silent, bounded-or-not, attacker-content overwrite β a strictly better primitive than DF-0820's same-field-coupled ones.) - :533-537 β maxcluster refill writes
NEW_ncgint32s past the array sized for OLD ncg. Reproduced (ncg 1β0x20000000):panic: vm_fault: fault on stack guard, store at ffs_mount+0xab9. - :504 β re-imports
mp->mnt_maxsymlinklen(DF-3015's symlink-overflow trust boundary gains a second import path). - :1292-1295 (
ffs_sbupdate, reached by RW-upgrade of the reloaded mount) βbcopy(fs, bp->b_data, fs->fs_sbsize)with adopted sbsize reads past the mount-timeum_fsallocation. Reproduced (sbsize 2048β65536): Fatal trap 12, supervisor READ, in memmove; and (sbsizeβ8192) the 6144 bytes beyondum_fslanded on the raw media (zero pages on the sparse guest β on a busy host, adjacent live heap objects). - Adoption in a returning reload proven independently: crafted
fs_dsize(sb+40) shows up indfoutput right aftermount(RELOAD).
Why this is a NEW finding (not DF-0820 re-filed)
DF-0820 = ffs_mountfs (initial mount) consumes unvalidated on-disk
geometry. DF-3078 = ffs_reload (MNT_RELOAD) fails to cross-check the
re-read sb against the ALREADY-ALLOCATED, previously-validated state. Same
family (mount-boundary census, cf. DF-3047 the ext2 MNT_RELOAD analog),
different function, different fix, and β unlike 0820 β the cssize variant
yields a content-and-length controlled, silently-completing overwrite.
Exploitability to uid=0 β BLOCKED (reachability, not primitive)
The write primitive itself (attacker bytes Γ attacker length, groomable: any
cssize delta gives a precise bounded overwrite past fs_csp) is LPE-grade
on this no-SMAP/SMEP/KASLR guest. But the only callers are mount(2)-based
paths: UFS mounts and their updates are gated by SYSCAP_RESTRICTEDROOT
(get_fscap, sys/kern/vfs_syscalls.c:5383+; empirically proven in the
DF-0820 pack β vfs.usermount=1 does NOT let a non-root user mount ufs).
So the trigger requires root; rootβkernel is not a privilege-boundary
crossing. An automount/daemon that mounts removable media as root would
make the trigger unprivileged β documented, not present on this guest.
Fix validation
fix.diff (in-pack): ffs_reload cross-checks the re-read sb's sizing
geometry (fs_sbsize/fs_bsize/fs_fsize/fs_frag/fs_ncg/fs_ipg/fs_cssize/
fs_csaddr/fs_contigsumsize/fs_bshift/fs_fshift) against the mounted one,
returning EINVAL on mismatch, and forces newfs->fs_fmod = 0.
Built as kernel #1 (make -j6 nativekernel KERNCONF=X86_64_GENERIC, BUILD_OK)
and validated (run.patched.log):
| variant | baseline #0 | patched #1 |
|---|---|---|
| fs_fsize=0 | Fatal trap 18 (idivl) | EINVAL, guest up |
| fs_ncg=0x20000000 | vm_fault stack-guard panic | EINVAL, guest up |
| fs_cssize=0x80000 | silent 522KB heap smash, rc 0 | EINVAL, guest up |
| fs_sbsize=65536+RW | trap 12 supervisor-read fault | EINVAL, guest up |
| fs_sbsize=8192+RW | 6KB heapβmedia | EINVAL, guest up, no leak bytes |
| unchanged reload | works | works (rc 0) |
| fresh RW mount + create | works | works |
fix_status: fixed.
Guest / environment
DragonFly 6.5-DEVELOPMENT #0 (baseline) / #1 (fix), X86_64_GENERIC,
INVARIANTS on, x86_64 (env.txt). Guest reset clean (vm.sh reset with-src)
after validation.
Fix verification
fixedfix.diff applied to in-guest /usr/src, make -j6 nativekernel KERNCONF=X86_64_GENERIC + installkernel (BUILD_OK), rebooted into #1: all five crafted variants now return 'mount: Invalid argument' (EINVAL) with the guest staying up and the filesystem still accessible; unchanged-sb reload returns 0; fresh RW mount + file create works. Before/after matrix in VERDICT.md and run.patched.log.
['findings/poc/DF-3078/run.patched.log', 'findings/poc/DF-3078/build.log', 'findings/poc/DF-3078/fix.diff']
Confirmed kernel references
- sys/vfs/ufs/ffs_vfsops.c:479
- sys/vfs/ufs/ffs_vfsops.c:484
- sys/vfs/ufs/ffs_vfsops.c:493
- sys/vfs/ufs/ffs_vfsops.c:495
- sys/vfs/ufs/ffs_vfsops.c:500
- sys/vfs/ufs/ffs_vfsops.c:515
- sys/vfs/ufs/ffs_vfsops.c:526
- sys/vfs/ufs/ffs_vfsops.c:536
- sys/vfs/ufs/ffs_vfsops.c:671
- sys/vfs/ufs/ffs_vfsops.c:685
- sys/vfs/ufs/ffs_vfsops.c:1292
- sys/vfs/ufs/ffs_vfsops.c:1295
Detail
Evidence (decisive lines)
['findings/poc/DF-3078/run.log β full baseline transcript (all variants)', 'findings/poc/DF-3078/panic_idivl_fsize0.txt β Fatal trap 18 idivl (fsize=0 adopted)', 'findings/poc/DF-3078/panic_maxcluster_ncg.txt β vm_fault stack-guard panic (ncg=0x20000000 refill store)', 'findings/poc/DF-3078/panic_sbupdate_oob_read.txt β trap 12 supervisor READ in memmove (sbsize=65536 sbupdate OOB read)', 'findings/poc/DF-3078/run.patched.log β fix validation: all crafted variants EINVAL on #1, regressions pass', 'findings/poc/DF-3078/build.log, env.txt, fix.diff']
PoC changes
Trigger written from scratch: DF's mount_ufs has no '-o reload' option, so the PoC calls mount(2) directly with MNT_UPDATE|MNT_RELOAD|MNT_RDONLY and a zeroed >=sizeof(struct ufs_args) data buffer (a first draft passed a too-small struct and would have made ffs_mount's copyin over-read user memory). craft.c/dumpsb.c built on-guest with -I/usr/src; sb field offsets verified in-guest (fs_cssize=156, fs_fsize=52, fs_ncg=44, fs_sbsize=104, fs_dsize=40, fs_fmod=208).
Verified recommended fix
ffs_reload: reject any re-read superblock whose sizing geometry differs from the mounted one (fs_sbsize/bsize/fsize/frag/ncg/ipg/cssize/csaddr/contigsumsize/bshift/fshift) with EINVAL, and force fs_fmod=0 β see fix.diff; validated on a single-fix kernel.
Verdict
REPRODUCED on stock #0: MNT_RELOAD (mount(2) MNT_UPDATE|MNT_RELOAD|MNT_RDONLY on a RO-mounted UFS) re-reads the superblock and adopts its geometry (ffs_vfsops.c:500) while fs_csp/fs_maxcluster/um_fs keep their mount-time sizes (allocated at :671/:685). Crafted re-read sb gives: heap OOB WRITE with fully attacker-controlled length AND content (cssize variant: 524288 pattern bytes bcopy'd into the 2053-byte fs_csp chunk, reload returned 0, ~522KB heap silently smashed); unbounded OOB write of controlled value (ncg variant: vm_fault stack-guard panic in the *lp++ refill); divide-by-zero (fsize=0: Fatal trap 18 idivl at ffs_mount+0x627); heap OOB READ past um_fs via adopted fs_sbsize at RW-upgrade ff_sbupdate bcopy (sbsize=65536: Fatal trap 12 supervisor-read in memmove; sbsize=8192: the 6144 bytes beyond um_fs written to the raw media). Adoption in returning reloads proven via crafted fs_dsize showing in df. NOT an unpriv->root LPE: trigger is root-gated (get_fscap maps ufs to SYSCAP_RESTRICTEDROOT; vfs.usermount=1 does not help β same blocker empirically proven in the DF-0820 pack). Impact ceiling: root-context kernel heap corruption + kernel-heap disclosure to media; panic DoS demonstrated.
No comments yet.