β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3171

mfsclose() deactivates the MFS I/O server on ANY device close β€” no finality check (speculative: would destroy_dev/kfree the serving mfsnode under a live mount if a non-final close ever slips through)

Summary

mfsclose() unconditionally clears mfs_active and wakes the server, assuming every close is the unmount-driven final close. devfs can invoke d_close for VRECLAIMED vnodes regardless of open count (devfs_vnops.c:1169-1171), and the close gate's opencount comes from count_dev(), which the kernel itself marks 'XXX NOT SMP SAFE' (:1102/:1148). A non-final close while mounted would kill the server, then destroy_dev (async) + immediate kfree(mfsp) leave the still-mounted fs issuing bios against a freed mfsnode (UAF). No reachable deterministic trigger demonstrated: FWRITE opens of a mounted mfs device return EBUSY and read-only opens migrate to the primary devfs vnode, so second-devfs vnode reclaims fire no d_close - three guest constructions all left the server alive (full honesty record; a stock panic initially attributed here was re-attributed to DF-3170). Residual exposure: the in-tree-acknowledged count_dev miscount or an equivalent opencount anomaly. Defense-in-depth guard built into validation kernels and proven harmless. Fix: mfsclose ignores closes while si_mountpoint != NULL.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3171 Β· 11 files
FileTypeDescriptionSize
trigger.sh β€” 1.5 KB view raw
mfsrace.c β€” 2.8 KB view raw
run.log β€” 2.6 KB view raw
panic_investigated_misattribution.txt β€” 812 B view raw
boot_serial.log β€” 13.6 KB view raw
env.txt β€” 517 B view raw
VERDICT.md β€” 4.0 KB ↓ raw
fix.diff β€” 948 B view raw
fix_build.log β€” 1.3 KB view raw
fix_run.log β€” 1.2 KB view raw
verdict.json β€” 3.5 KB view raw
VERDICT.md
↓ download raw

DF-3171 β€” VERDICT

Status: not_reproduced (for the hypothesized trigger). The code hazard is real but no reachable trigger was demonstrable; see the honesty record below. Confidence: speculative. Impact: none demonstrated.

What was hypothesized

mfsclose() (mfs_vfsops.c:136-147) clears mfs_active unconditionally, taking the I/O server down on ANY device close. devfs calls d_close on VRECLAIMED vnodes regardless of the remaining open count (devfs_vnops.c:1169-1171), so a non-final close (vnode reclaim while the mount still holds the device) would kill the server under a live mount β†’ destroy_dev (async, devfs_core.c:754-763) + kfree(mfsp) (mfs_vfsops.c:494-498) β†’ still-mounted filesystem doing I/O through a freed mfsnode (UAF).

What actually happened (honesty record)

  1. First stock attempt (~15:56): exec 8<>/mnt/d2/mfs853 + umount -f. The box panicked with Bad tailq NEXT(...->tqh_last) != NULL from mfsstrategy β€” initially filed as this finding's reproduction. That attribution was wrong: the 8<> (O_RDWR) open of a mounted-RW D_DISK device fails EBUSY in devfs_specopen (devfs_vnops.c:1006-1020), so no fd existed and no d_close ever fired. The panic was DF-3170's producer/consumer race, which the concurrently-running writers were exercising (full reanalysis in findings/poc/DF-3170/run.2.log β€” the panic evidence has been reattributed to DF-3170).
  2. Second stock attempt (read-only open, fd held by a short-lived sh -c): the fd closed when the shell exited before umount β€” vclean had no opencount to force-close. Server survived.
  3. Third stock attempt (read-only fd held by a sleep 90 holder process across umount -f /mnt/d2): umount succeeded, server survived, mount healthy, I/O fine β€” because devfs device opens migrate to the device's PRIMARY vnode (devfs_specopen's orig_vp handling, devfs_vnops.c:956-1000 β€” "the returned ... might not be using the vp we supplied"), leaving the second-devfs vnode with zero open count; its reclaim fires no VOP_CLOSE, and the primary vnode (in the main /dev devfs) is never reclaimed while the system runs.

Why this is still filed (speculative, Low)

The hazard is one miscount away: devfs_vnops.c:1148 and :1102 compute the close gate with count_dev(dev) and are annotated "XXX NOT SMP SAFE" in-tree. A miscount that yields a false opencount == 1 on some close while the mount holds another reference makes mfsclose fire early with exactly the consequences hypothesized (server exit under a live mount, destroy_dev + kfree of the serving mfsnode, bios on freed memory). The in-tree comment at mfs_vfsops.c:120-121 ("We allow the underlying MFS block device to be opened and read") makes stray opens an expected condition. This reviewer could not win that race on demand in the time budget; no reachable deterministic trigger exists via the devfs#2 construction.

Fix (defense-in-depth, validated harmless)

fix.diff: mfsclose() ignores closes while dev->si_mountpoint != NULL (ffs_unmount clears it before the real final close, ffs_vfsops.c:851-855). Validated on a patched in-guest kernel (#1 Mon Sep 7 16:11:15 2026 and rebuilt #2 16:51:08): - mount / I/O / normal unmount lifecycle fully intact (umount rc=0, mount gone, server exited via EMOUNTEXIT, /dev/mfsN removed); - read-only-fd + forced devfs umount construction: server survives, mount continues serving I/O; - 11+ minutes of DF-3170 stress (plus ENOSPC churn) across multiple concurrent MFS mounts: no regression.

Since the bug itself never manifested, fix validation is recorded as inconclusive (guard proven safe and lifecycle-preserving; bad behavior absent on both stock and patched).

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.

Fix verification

inconclusive
baseline no→ patch + rebuild →patched clean

The guarded behavior could not be shown bad-then-good because no construction manifested the bug on the stock kernel (baseline_reproduced=0 by absence of trigger, not by fix). The guard was proven safe: normal mount/IO/umount lifecycle intact on both patched builds, kill constructions ignored, 11+ min stress without regression.

findings/poc/DF-3171/fix_run.log, findings/poc/DF-3171/fix_build.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Sep 7 16:51:08 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.log (all three trigger attempts + stock normal-teardown control)', 'VERDICT.md (attribution retraction + remaining count_dev race rationale)', 'panic_investigated_misattribution.txt (the reattributed panic)', 'fix_run.log (guard validation matrix)']

PoC changes

Trigger revised three times while chasing the mechanism: FWRITE open -> read-only open with short-lived holder -> read-only open with persistent (sleep 90) holder across umount -f; each stage's outcome recorded; the open-vnode-migration discovery explains all negatives.

Verified recommended fix

mfsclose: ignore closes while dev->si_mountpoint != NULL (ffs_unmount clears it before the true final close).

Verdict

The mfsclose hazard is code-real (mfs_active cleared on any device close; devfs can call d_close on VRECLAIMED vnodes or on a count_dev() miscount the kernel itself marks 'XXX NOT SMP SAFE'), but no reachable trigger was demonstrable: FWRITE opens of a mounted mfs device return EBUSY (devfs_vnops.c:1006-1020), and read-only opens migrate to the device's primary devfs vnode so reclaiming a second-devfs vnode never fires d_close. Three trigger constructions on the stock QEMU kernel all left the server alive and the mount healthy. The stock panic initially attributed to this finding was reattributed to DF-3170 after tracing the EBUSY open failure (full honesty record in VERDICT.md/run.log). Filed as Low/speculative hardening; the si_mountpoint guard in mfsclose was built and validated as harmless (normal mount/IO/umount lifecycle fully intact on two patched kernels).