# 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:

1. re-reads the superblock from the device (`bread(devvp, SBOFF, SBSIZE)`, :479);
2. validates **only** `fs_magic` and `fs_bsize` bounds (:484-488) — the same
   two fields `ffs_mountfs` checks (DF-0820);
3. `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`, …) while `fs->fs_csp` (:495/:516), the
   `fs_maxcluster` array and `fs_contigdirs` still point into the
   **mount-time allocation sized for the OLD geometry** (`ffs_mountfs`
   :680-709: `cssize + ncg*4 + ncg*1` bytes, 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's `mount_ufs` has no `-o reload` support, 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 signatures
- `fix.diff` — geometry cross-check in ffs_reload (+ force `fs_fmod=0`);
  validated: all crafted variants → clean `EINVAL`, 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.
