# 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_bsize` bounds.
- :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
  adopted `fs_fsize==0`. **Reproduced: Fatal trap 18, idivl, ffs_mount+0x627.**
- :515-529 — step-3 loop re-reads `howmany(NEW_cssize, NEW_fsize)` frags of
  `NEW_csaddr` into the OLD `fs_csp`: `bcopy(bp->b_data, space, size)` walks
  `space` forward — 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_ncg` int32s 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-time `um_fs` allocation. **Reproduced (sbsize 2048→65536): Fatal
  trap 12, supervisor READ, in memmove; and (sbsize→8192) the 6144 bytes
  beyond `um_fs` landed 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 in `df` output right after `mount(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.
