# DF-3190 — VERDICT

**status: reproduced / impact: panic (deterministic) / confidence: certain**
primitive: kernel buffer undersized by superblock-controlled arithmetic, then
overwritten with an attacker-sized, attacker-contented copy — plus a
deliberate consistency panic on the error-cleanup path.  Fix validated:
crafted image rejected at mount on the patched kernel; stock image unaffected.

## Root cause (line-accurate)

1. `sys/vfs/ufs/ffs_vfsops.c:641-646` — mount checks only `fs_magic` and
   `fs_bsize ∈ [sizeof(struct fs), MAXBSIZE]`.  `fs_fmask`/`fs_qfmask`
   (and `fs_bmask`/`fs_qbmask`/shifts) are copied into the live `struct fs`
   verbatim (`fs_qfmask` via `bcopy` of the superblock, `ffs_vfsops.c:667-674`).
2. `sys/vfs/ufs/ffs_vfsops.c:812-815` — `ffs_oldfscompat` recomputes the
   masks only when `fs_inodefmt < FS_44INODEFMT`; any image newfs(8) produces
   today keeps the crafted values.
3. `sys/vfs/ufs/fs.h:506` — `fragroundup(fs,size) = ((size)+fs_qfmask) &
   fs_fmask` is pure attacker arithmetic with those two fields.
4. `sys/vfs/ufs/ffs_balloc.c:193-202` — fresh direct block:
   `nsize = fragroundup(fs, size)` (size == fs_bsize for the PoC write)
   → `ffs_alloc(nsize)` allocates `numfrags(nsize)` frags →
   `getblk(vp, lbn, nsize)` — **buffer `nsize` bytes for a `size`-byte write**.
   Sibling sinks: `:168-171` (`osize`/`bread` path), `:119/:129`
   (`blksize()`/`ffs_realloccg`), ffs_fragextend's `cg_frsum` store
   (`ffs_alloc.c`, secondary).
5. `sys/vfs/ufs/ufs_readwrite.c:346-354` — `uiomovebp(bp, bp->b_data +
   blkoffset, xfersize, uio)` with `xfersize == fs_bsize` >> buffer size.

## What was run (guest `DragonFly dfbsd 6.5-DEVELOPMENT #0`,
X86_64_GENERIC, INVARIANTS on, DIAGNOSTIC off)

| run | image | trigger | result |
|---|---|---|---|
| `run.baseline.log` | stock sb | uid=1001 `write(16384)` | success, file 16384 B, no panic |
| `run.log` + `console.craft1.txt` | poisoned | uid=1001 `write(16384)` | **panic** (cpuid 5) |
| `run.2.log` + `console.craft2.txt` | poisoned | same, fresh boot | **panic** (cpuid 0) |
| `console.craft3.txt` | poisoned | same, fresh boot | **panic** (cpuid 3) |
| `run.patched.log` | poisoned, kernel #1 w/ fix | same | mount rejected (`incorrect super block`), no panic, guest up |
| `run.patched.baseline.log` | stock sb, kernel #1 w/ fix | uid=1001 `write(16384)` | success — no regression |

Craft: `fs_fmask = 0x1fff`, `fs_qfmask = 0x800` on a stock newfs image
(bsize 16384, fsize 2048, frag 8) ⇒ `fragroundup(16384) = 2048`.

## Panic chain (why it panics in ffs_blkfree and not in uiomove)

The oversized copy *is* attempted: `uiomove` → `copyin(user, b_data, 16384)`
(`sys/kern/kern_subr.c:136`).  The buffer is VMIO with one page mapped;
bytes 2048..4095 overflow into that page's tail, and at `b_data+4096` the
copy faults.  DragonFly's `copyin` has an on-fault window (returns EFAULT on
any fault during the copy) so the wild write is **converted to EFAULT
instead of trapping**.  `ffs_write` then runs its IO_UNIT error path
(`ufs_readwrite.c:404-410`) → `ffs_truncate(vp, osize=0)`; but `ip->i_size`
was already advanced to 16384 (`ufs_readwrite.c:348-350`) before the failed
copy, so the truncate frees `i_db[0] = 1041` as a *full block*
(`size = 16384`), and `ffs_blkfree_cg` (`ffs_alloc.c:1504-1507`) panics on
`fragoff(fs, 16384) = 16384 & 0x800 ≠ 0` under the poisoned `fs_qfmask`:

```
dev=vn0, bno = 1041, bsize = 16384, size = 16384, fs = /mnt
panic: ffs_blkfree: bad size
ffs_blkfree_cg → ffs_blkfree → ffs_truncate → ffs_write → vop_write
```

## Exploitability ceiling (documented hard blockers for uid=0 via this sink)

The primitive is a genuine attacker-sized/-contented write directed at
kernel KVA (`b_data + [0, fs_bsize)`, buffer of `nsize` bytes).  Two
mechanisms, verified in source, cap it at EFAULT/panic on this kernel:

1. **Per-buffer KVA reservation** — every VMIO buffer owns a `MAXBSIZE`
   (64 KB) KVA reservation (`sys/kern/vfs_bio.c:640`), and `allocbuf` maps
   only `desiredpages = pages(bufsize)` (`vfs_bio.c:3038-3057`,
   `pmap_qenter` only for the buffer's own pages).  Writes past the buffer's
   last mapped page land in *its own unmapped reservation*, never in a
   neighbour buffer — cross-buffer grooming is structurally impossible via
   this overflow.  The only silent-overflow window is
   `bufsize → roundup(bufsize, PAGE_SIZE)` (≤ 4095 B into the buffer's own
   last page) — same `vm_page` as the buffer itself, no cross-object effect.
2. **`copyin` on-fault window** (`sys/kern/kern_subr.c:136` with DFBSD's
   `copyin` fault handler) — every user→kernel copy through `uiomove` is
   fault-tolerant, so the out-of-mapping portion returns EFAULT rather than
   `Fatal trap 12`.  (Kernel-internal stores like DF-0894's `bap[in_off]`
   have no such window — different finding, different sink.)

Secondary sink (same root cause, cross-file): `ffs_fragextend`
`cgp->cg_frsum[i - numfrags(fs, osize)]--` — with stock `fs_fshift` the index
stays within ±`fs_frag` of the array (intra-`struct cg` corruption, silent
allocator-metadata corruption); crafting `fs_fshift` to widen the index is
gated by the early `cs_nffree < numfrags(bsize-osize)` bail-out
(`ffs_alloc.c` fragextend entry), which rejects the enlarged request before
the store.  Capped at intra-buffer corruption / DoS on this kernel.

Net: **reliable unprivileged-triggered kernel panic (DoS)** once the crafted
image is mounted, with a demonstrated buffer-size-decoupling memory-safety
violation whose silent-corruption routes are individually blocked and
documented above.  `uid=0` not achieved from this sink on this guest —
blocked by (1)+(2), not by lack of primitives.

## Fix validation

`fix.diff` (authored against the read-only `sys/` tree; applied only inside
the guest's `/usr/src` copy): validates in `ffs_mountfs` that
`fs_fsize`/`fs_frag`/`fs_bmask`/`fs_fmask`/`fs_qbmask`/`fs_qfmask`/
`fs_bshift`/`fs_fshift`/`fs_fragshift` are exactly the values recomputed
from `fs_bsize`/`fs_fsize` (power-of-two chain, `fs_frag ≤ MAXFRAG`),
else `EINVAL`.

* `make nativekernel KERNCONF=X86_64_GENERIC` — clean (`-Werror`), RC=0
  (`build.log`); installed as kernel **#1 Tue Sep 8 05:12:48 UTC 2026**.
* Crafted image → `mount_ufs: /dev/vn0 on /mnt: incorrect super block`,
  dmesg `ffs_mountfs: inconsistent fragment geometry`, **no panic**,
  trigger unreachable (`run.patched.log`).
* Stock image → mounts, uid=1001 `write(16384)` succeeds, 16384-byte file
  (`run.patched.baseline.log`).  No regression.

## Notes

* DF-0894 (crafted `fs_nindir`, `bap[indirs[i].in_off]` OOB in this same
  function) is **not fixed** in this tree — `fs_nindir` is still copied
  unvalidated (`ffs_vfsops.c:729`) — and is not re-reported here; the
  `fs_nindir` companion check belongs to DF-0894's fix.
* The `fs_maxbpg==0` divide-by-zero in `ffs_blkpref:805` (reachable from
  `ffs_balloc` on every allocation) is the DF-0820 umbrella family and is
  not re-filed.
* Guest reset to the clean-source snapshot after validation.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
