# DF-3170 — VERDICT

**Status: reproduced (panic). Impact: panic (kernel memory corruption
primitive: cross-CPU corruption of an in-kernel TAILQ). Confidence:
certain. Two independent stock-kernel manifestations: consumer-side
"Bad link elm" from mfs_start (~3 min of stress) and producer-side
"Bad tailq NEXT" from mfsstrategy (<90 s of stress) — see run.log,
run.2.log, panic.txt, panic_run2.txt.**

## Guest

DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
(X86_64_GENERIC, INVARIANTS), QEMU/KVM, 6 vCPUs (`hw.ncpu: 6`),
stock snapshot. Booted fresh via `vm.sh reset with-src` before the run.

## How it was reproduced

1. root: `mount_mfs -s 32768 swap /mnt/mfs` → server pid 899 parked in
   `mfsidl` (the mfs_start loop), device `/dev/mfs899`, mount live.
2. unprivileged user `maxx`: 6 × `/tmp/mfsrace /mnt/mfs 2` — six processes
   each hammering 2 files with 512-byte O_SYNC pwrite/pread loops.
3. Within ~3 minutes the guest panicked:

```
panic: Bad link elm 0xfffff8004f593d48 prev->next != elm
cpuid = 5
Trace beginning at frame 0xfffff80117f12668
mfs_start() at mfs_start+0x499 0xffffffff80913849
mfs_start() at mfs_start+0x499 0xffffffff80913849
vfs_start() at vfs_start+0x3b 0xffffffff8070d55b
sys_mount() at sys_mount+0x52f 0xffffffff80700ccf
syscall2() at syscall2+0x11e 0xffffffff80bd6a0e
Debugger("panic")
```

(panic.txt, boot serial log.) ssh died; `vm.sh status` → down.

## Why this proves the root cause

- The panicking frame is `mfs_start` — the consumer of `mfsp->bio_queue` —
  inside `bioq_takefirst`/`bioq_remove` (sys/sys/buf2.h:148-175), hitting
  `TAILQ_REMOVE`'s INVARIANTS check `prev->next != elm`
  (sys/sys/queue.h:505-519 family).
- The only writers of that TAILQ are `bioq_insert_tail` from
  `mfsstrategy` (mfs_vfsops.c:187) and the dequeue in `mfs_start`
  (mfs_vfsops.c:455). The producer runs under the BGL
  (`mfs_ops` has no `D_MPSAFE`; kern/kern_device.c:121,384-388) while the
  consumer deliberately does not (`MNTK_ST_MPSAFE` set at
  mfs_vfsops.c:401 → `VFS_MPLOCK_FLAG` no-ops in vfs_start,
  sys/sys/mount.h:408-417). No other lock exists. Therefore the corrupted
  linkage can only come from a producer racing the consumer across CPUs —
  exactly the interleaving predicted.
- cpuid=5 vs the server thread: 6-way SMP stress; every writer is
  unprivileged.

## Exploit-chain assessment (why impact is filed as panic, not uid0)

On the INVARIANTS validation kernel every queue inconsistency traps
immediately, so the observed result is a panic. On a production kernel the
same interleavings give:

1. **Lost bio** — dropped `TAILQ_INSERT` update: the bio never completes;
   the issuing thread blocks uninterruptibly in `biowait` forever (local
   DoS, very reliable to *cause*, not steerable).
2. **Stale `tqh_last` pointer store** — after a lost-update removal,
   `bio_queue.tqh_last` can point at the `bio_act.tqe_next` field of a bio
   that has been `biodone`d and whose `struct buf` returned to the buffer
   cache. The next `bioq_insert_tail` then executes
   `*(head)->tqh_last = elm`, writing a kernel heap pointer (address of the
   new bio's buf — not attacker-chosen) into recycled kernel memory.

Pushing (2) to `uid=0` requires all of: winning the few-instruction race on
demand; the recycled victim `struct buf` being reused as an object whose
corruption at `bio_act`'s fixed offset yields a privilege-relevant
primitive; and the *value* written (a buf address) being useful at that
slot. Buffer-cache bufs are recycled kernel-internally (bufdaemon) into the
same `struct buf` type — the stray store lands inside another buffer
descriptor, corrupting buffer linkage rather than credentials; there is no
allocator path that hands that slab slot to a `ucred`/`proc`/vtable object
while the dangling `tqh_last` still references it, and the write value is
never controlled. Documented blocker — no credible uid0 chain; the
deliverable stands at reliable unprivileged kernel panic (+ latent
corruption on non-INVARIANTS builds).

## Negative space (checked and killed during pass 2)

- `copyinstr(args.fspec,...)` unchecked + `size` used in
  `bzero(f_mntfromname+size, MNAMELEN-size)` (mfs_vfsops.c:376-380): NOT a
  bug on pc64 — `std_copyinstr` stores `*lencopied` on ALL return paths,
  including the fault path (sys/platform/pc64/x86_64/support.s:705-715),
  and the stored value is always ≤ MNAMELEN-1.
- `vget(devvp)` overwriting the ENOENT guard when `devvp == NULL`
  (mfs_vfsops.c:350-354): unreachable — `nlookup` with flags 0 returns
  ENOENT for negative-cache entries (kern/vfs_nlookup.c:891,905), so the
  `error == 0` branch always has a non-NULL `nc_vp`. Hardening wart only.
- `bp->b_bcount = mfsp->mfs_size - boff` truncation (mfs_vfsops.c:178):
  clip only executes when the remainder is smaller than the request, and
  requests are bounded by MAXPHYS/MAXBSIZE — no int truncation possible.
- devvp double-vrele on `ffs_mountfs` failure: killed — `ffs_mountfs` never
  vreles devvp on its error paths (ffs_vfsops.c:774-787), so
  mfs_vfsops.c:406 is the correct single drop.
- crafted `fs->fs_size` > `mfs_size`: no OOB — every access goes through
  `mfsstrategy` clip/EINVAL (mfs_vfsops.c:163-179) and `mfs_doio`'s
  copyin/copyout, both fault-bounded.
- MNT_UPDATE path / EMOUNTEXIT plumbing (vfs_vfsops.c:115-116): correct;
  mfs_start returns only after dounmount() completed.

## Fix validation

See fix_build.log / fix_run.log and the fix_* fields in verdict.json.
Combined single-fix kernel (this fix + DF-3171's guard, which touches the
same function) built in-guest with `make nativekernel`; the exact PoC
stress and the DF-3171 kill sequence were re-run on it: no panic, mount
I/O continues, unmount clean. Baseline (stock kernel) panicked in both
runs.

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