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

hammer2_cleanup_devvp() unconditionally clears dev->si_mountpoint, stripping the kernel's raw-device write gate and double-mount protection from live hammer2 devices and from other filesystems' devices

Summary

hammer2_cleanup_devvp (ondisk.c:225-226) sets v_rdev->si_mountpoint=NULL on every device entry with no ownership check, and is called with foreign or still-live devices on two reachable paths: the routine secondary-PFS mount path (vfsops.c:1358-1360 - cleaned BEFORE the label lookup, so both successful and FAILED label lookups strip the marker from a live device; default behavior on any multi-PFS hammer2 system) and the vfs_mountedon EBUSY error path (vfsops.c:1092-1099 - clears markers set by ufs/msdosfs/ext2fs/cd9660/ntfs/hpfs/udf). si_mountpoint is the sole state behind vfs_mountedon (the double-mount guard) and the devfs mounted-device write gate (open(O_WRONLY) of a mounted RW block device returns EBUSY). Guest-verified: dd went EBUSY -> success (64KB and full-200MB zeroing under two live RW hammer2 mounts after a routine @LOCAL mount or a FAILED @NOSUCH mount); cross-fs variant flips a live UFS device from EBUSY to writable via one failed hammer2 mount. Marker lifecycle also broken upstream (unmounting the first mount leaves the devfs gate dereferencing a freed mount; ffs softdep consumers lose their pointer). HAMMER1 performs the correct ownership check (hammer_ondisk.c:362-366); hammer2 never received it. Converts mounted media into attacker-writable state for any credential with node write permission - upgrading every on-disk-parser bug into a reachable primitive. Ownership-checked marker management fix validated by full guest kernel rebuild: all three dd bypasses return EBUSY, all mounts still succeed.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5799 Β· 11 files
FileTypeDescriptionSize
README.md β€” 3.8 KB ↓ raw
VERDICT.md β€” 7.4 KB ↓ raw
poc.sh β€” 2.6 KB view raw
build.sh β€” 83 B view raw
run.sh β€” 125 B view raw
run.log β€” 1.5 KB view raw
run.fixed.log β€” 1.4 KB view raw
build.log β€” 5.6 MB ↓ download
env.txt β€” 226 B view raw
fix.diff β€” 5.5 KB view raw
verdict.json β€” 4.6 KB view raw

DF-5799 β€” hammer2_cleanup_devvp() strips si_mountpoint from live/foreign devices

File: sys/vfs/hammer2/hammer2_vfsops.c (+ sys/vfs/hammer2/hammer2_ondisk.c) Class: protection removal / cross-filesystem state corruption (Dillon bucket: hammer2) Severity: High Β· Confidence: certain Β· Status: REPRODUCED (and fix validated)

What

hammer2_cleanup_devvp() (sys/vfs/hammer2/hammer2_ondisk.c:216-235) sets e->devvp->v_rdev->si_mountpoint = NULL on every device entry it processes, unconditionally. It is reached on paths where the devices are not hammer2's to clean up:

  1. Routine secondary PFS mount β€” hammer2_vfs_mount() matches the device to an existing live hmp and then calls hammer2_cleanup_devvp(&devvpl) (sys/vfs/hammer2/hammer2_vfsops.c:1358-1360) on the caller-supplied device list. The devices still back a live, mounted hammer2 hmp. The cleanup strips their si_mountpoint marker. On any multi-PFS hammer2 installation (the standard DragonFly layout: /, /var, /tmp, … are separate PFS labels on one device), the second mount of the device silently disables the kernel's raw-device write protection for as long as any mount from that device exists. Even a failed secondary mount attempt (unknown label) strips the marker, because cleanup runs before the label lookup.
  2. Failed mount over a foreign filesystem's device β€” when vfs_mountedon() rejects the mount (sys/vfs/hammer2/hammer2_vfsops.c:1092-1099), the error path calls hammer2_cleanup_devvp(&devvpl) on devices whose si_mountpoint was set by another filesystem (ufs, msdosfs, ext2fs, cd9660, ntfs, hpfs, udf all set it), clearing their marker.

Why it matters

si_mountpoint is the only state behind two kernel protections:

  • vfs_mountedon() (sys/kern/vfs_subr.c:1887-1895) β€” the double-mount guard used by ffs, msdosfs, ext2fs, cd9660, ntfs, hpfs, udf, and hammer2.
  • The devfs raw-write gate (sys/vfs/devfs/devfs_vnops.c:1007-1022): any open(O_WRONLY) of a mounted block device returns EBUSY unless the mount is read-only (uid-independent; only FSCRED bypasses). After the marker is stripped, any subject with write permission on the device node can overwrite the media under a live read-write mount β€” including crafting on-disk metadata that the kernel's filesystem parsers then consume, and breaking ffs softdep assumptions (sys/vfs/ufs/ffs_softdep.c:4266-4267 dereferences si_mountpoint).

Additionally, the marker lifecycle upstream holds the struct mount * of the first mount; when that mount unmounts while later mounts keep the device, the marker points at a freed mount β€” the devfs gate then reads si_mountpoint->mnt_flag out of freed memory (stale-pointer read). HAMMER1 fixed the ownership aspect correctly (sys/vfs/hammer/hammer_ondisk.c:362-366 clears only when si_mountpoint == hmp->mp); hammer2 never got that fix.

Reproduce (guest-live)

scp poc.sh dfbsd:/root/ && vm.sh run_root 'csh /root/poc.sh'

Expected output on a vulnerable kernel (see run.log):

[3] dd: /dev/vn0: Device busy          # gate active after first mount
[4] mount -t hammer2 /dev/vn0@LOCAL … mount rc=0   (routine secondary mount)
[5] 65536 bytes transferred … dd rc=0  # BUG: raw write under TWO live mounts
[6] (failed @NOSUCH mount)   dd rc=0   # BUG: even a failed attempt strips it
[7] dd: /dev/vn1: Device busy          # UFS gate active
[8] mount -t hammer2 /dev/vn1@X … "Device busy"   (failed h2 attempt)
[9] 65536 bytes transferred … dd rc=0  # BUG: raw write under live UFS mount

On the patched kernel (see run.fixed.log) every dd returns Device busy at steps 5/6/9 while all mounts still succeed β€” fix validated by full kernel rebuild (build.log, fix.diff).

VERDICT.md
↓ download raw

DF-5799 VERDICT β€” REPRODUCED (protection removal; fix validated)

Bottom line

hammer2_cleanup_devvp() (sys/vfs/hammer2/hammer2_ondisk.c:225-226) clears v_rdev->si_mountpoint unconditionally for every device in the list it is handed. Two reachable call sites hand it devices whose markers belong to someone else or to a still-live hammer2 mount:

  • sys/vfs/hammer2/hammer2_vfsops.c:1358-1360 β€” secondary-PFS mount path (devvp matched an existing hmp β†’ hammer2_cleanup_devvp(&devvpl) before the label lookup). Runs on every secondary hammer2 mount attempt from a device that is already mounted β€” success OR failure of the label lookup.
  • sys/vfs/hammer2/hammer2_vfsops.c:1092-1099 β€” vfs_mountedon() EBUSY path: the devvpl devices belong to a foreign filesystem (ufs/msdos/ext2/cd9660/ ntfs/hpfs/udf); cleanup strips their marker.
  • (aggravating, same lifecycle) sys/vfs/hammer2/hammer2_ondisk.c:691 parks the first mount's struct mount * in the marker; unmounting that mount while later mounts keep the device leaves a stale pointer that the devfs write gate dereferences (sys/vfs/devfs/devfs_vnops.c:1019 reads si_mountpoint->mnt_flag) β€” a use-after-free read of struct mount.

vfs_mountedon() (sys/kern/vfs_subr.c:1887-1895) is a bare si_mountpoint != NULL test, and it is the only double-mount guard for most filesystems and the only thing behind the devfs raw-write gate. HAMMER1 does the ownership check correctly at sys/vfs/hammer/hammer_ondisk.c:362-366 (si_mountpoint == hmp->mp), which is the upstream-blessed shape of the fix.

How it was reproduced (baseline, stock kernel #0, 2026-10-12)

Run ./run.sh (transcript: run.log):

  1. vnconfig a 200 MB file as /dev/vn0; newfs_hammer2 -L ROOT.
  2. mount -t hammer2 /dev/vn0@ROOT /mnt/a β†’ rc=0.
  3. dd if=/dev/zero of=/dev/vn0 bs=64k count=1 β†’ "Device busy" (devfs gate active; si_mountpoint set by hammer2_init_volumes).
  4. mount -t hammer2 /dev/vn0@LOCAL /mnt/b β†’ rc=0 (routine secondary PFS mount). Console shows hmp=… matched then pmp=… β€” the hammer2_cleanup_devvp(&devvpl) at vfsops.c:1360 has already run.
  5. Same dd β†’ succeeds (65536 bytes). The write protection of a device backing TWO live read-write hammer2 mounts is gone.
  6. umount /mnt/b; mount -t hammer2 /dev/vn0@NOSUCH /mnt/b β†’ fails ("not found") β€” yet the same dd still succeeds: even a failed secondary mount attempt strips the gate (cleanup precedes the label lookup).
  7. Cross-fs variant: UFS on /dev/vn1 (mount -t ufs rc=0); dd of=/dev/vn1 β†’ Device busy.
  8. mount -t hammer2 /dev/vn1@X /mnt/b2 β†’ "Device busy" (the vfs_mountedon path at vfsops.c:1092-1099; its cleanup clears UFS's marker).
  9. Same dd of=/dev/vn1 β†’ succeeds β€” raw write-open under a live read-write UFS mount. (A second UFS mount is still blocked by ffs's independent vcount(devvp) guard at sys/vfs/ufs/ffs_vfsops.c:608-609; the universal raw-write gate is what hammer2 broke.)

Full-device impact demo (first exploratory run, transcript in this audit session): after step 5, dd if=/dev/zero of=/dev/vn0 bs=1m count=200 completed β€” 200 MB of the live-mounted device zeroed with no EBUSY; the mounted fs keeps serving from cached chains, on-disk state destroyed (corruption surfaces on cache eviction/remount; ffs softdep's si_mountpoint consumers at ffs_softdep.c:4184,4266 also lose their mount pointer).

Exploit chain / impact ceiling

  • Trigger: one hammer2 mount attempt touching the victim device (root, or a vfs.usermount user for devices they own; on multi-PFS hammer2 systems the trigger is routine admin behavior, and it also fires on failures).
  • Effect: open(O_WRONLY) of the mounted raw device becomes possible for ANY credential with node write permission (the devfs gate is uid-blind). That converts "mounted filesystem media" into attacker-writable state: crafted metadata under live kernel parsers (hammer2/ffs parsing bugs become memory-corruption primitives), softdep state corruption, silent data destruction; and un-blocks double-mounts for filesystems lacking ffs's vcount guard.
  • Not a direct uid=0 chain by itself β€” it is a protection-removal that upgrades the reachable attack surface of every filesystem mounted on the device. Classified impact: dos (device/fs destruction) with privesc-facilitation.

Fix validation (mandatory per contract β€” done)

fix.diff (git-applyable, in this pack):

  • removes the unconditional clear from hammer2_cleanup_devvp;
  • manages the marker explicitly: hammer2_devvp_mount(hmp, mp) (re-own on every mount / cluster-join in hammer2_mount_helper and hammer2_pfsalloc), hammer2_devvp_unmount(hmp, mp) (ownership-checked clear on last unmount), hammer2_devvp_unmount_all(hmp) (final device teardown), plus marker cleanup in hammer2_init_volumes' error path. This also eliminates the stale-struct mount * dereference window.

Applied inside the guest (git apply), make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0 (build.log), make installkernel, reboot into kernel #1 (2026-10-12 21:32:58), re-ran the identical poc.sh (run.fixed.log):

step stock kernel #0 patched kernel #1
[3] gate before secondary mount Device busy Device busy
[4] secondary mount @LOCAL rc=0 rc=0 (no regression)
[5] dd after secondary mount rc=0 (BUG) Device busy
[6] dd after FAILED secondary mount rc=0 (BUG) Device busy
[9] dd under UFS after failed h2 mount rc=0 (BUG) Device busy

Fix status: fixed (baseline reproduced, patched kernel suppresses all three write-gate bypasses; mounts/unmounts still function).

Negative results (pass-3 candidates investigated and killed)

  • multi-volume sparse volu_id β†’ hammer2_get_volume panic: killed β€” hammer2_verify_volumes_2 (hammer2_ondisk.c:390-405) forces volu_loff[i]==-1 for i >= nvolumes, and verify_common rejects offset == -1, so ids are provably dense {0..nvolumes-1}.
  • unbounded bzero(f_mntfromname + size, MNAMELEN - size) at hammer2_vfsops.c:1497-1499 on copyinstr fault: killed β€” DFly std_copyinstr (sys/platform/pc64/x86_64/support.s:655-718) always writes *lencopied (0 on immediate fault, ≀ maxlen otherwise).
  • hammer2_vfs_vget "uninitialized ip": killed β€” ip is initialized by hammer2_inode_lookup() at hammer2_vfsops.c:1915.
  • !hmp->devvp teardown of zero-initialized vchain/fchain (hammer2_vfsops.c:1161-1167): unreachable β€” hammer2_init_volumes returns EINVAL ("root volume not found") whenever *rootvoldevvp unset.
  • statfs/statvfs leaving *sbp unwritten when nchains == 0 (hammer2_vfsops.c:2047-2081): unreachable while a pmp is mounted (mount requires β‰₯1 chain; the pfs_nmasters > nchains wedge is DF-2630).
  • hammer2_remount iterating hmp->nvolumes vs sparse hmp->volumes[]: same density proof as above kills it.
  • hammer2_pfsdealloc not clearing iroot->cluster.focus/pfs_names: only reachable for unmounted pmps via ioctl; no deref path found (speculative, not reported).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied in guest /usr/src, kernel rebuilt (nativekernel rc=0), installed, rebooted; identical poc.sh re-run: dd returns 'Device busy' at all previously-bypassed steps ([5],[6],[9]) while primary+secondary PFS mounts and UFS mount all still succeed β€” bypass eliminated, no functional regression.

findings/poc/DF-5799/fix.diff (git-applyable); build.log (35772 lines, clean); run.fixed.log vs run.log comparison table in VERDICT.md
↓ fix.diffper-fix-DF-5799

Confirmed kernel references

Detail

Exploit chain

1) any hammer2 mount attempt touching the victim device (root, or usermount-privileged user; routine multi-PFS mounting triggers it silently, and failures trigger it too) -> 2) cleanup_devvp clears the device's si_mountpoint -> 3) open(/dev/, O_WRONLY) now passes the devfs mounted-device gate (uid-independent) -> 4) arbitrary raw writes under live RW mounts (hammer2 or foreign fs): crafted on-disk metadata into kernel parsers, softdep state corruption, device/fs destruction; double-mount un-blocked for filesystems without ffs's vcount guard.

Evidence (decisive lines)

run.log steps [3]/[5]/[6]/[9]: dd 'Device busy' -> rc=0 after secondary mount / failed secondary mount / failed h2-mount over UFS device; run.fixed.log: all three return 'Device busy' on the patched kernel with mounts still working; build.log: clean kernel build with fix.diff; VERDICT.md table.

PoC changes

poc.sh is a self-contained csh script (vnconfig + newfs_hammer2 + mount + dd gate probes); no seed code was provided for this pass-3 finding.

Verified recommended fix

Remove the unconditional si_mountpoint clear from hammer2_cleanup_devvp(); manage the marker with ownership-checked helpers (hammer2_devvp_mount/_unmount/_unmount_all) called from mount/unmount mount_count transitions, mirroring hammer1's si_mountpoint == hmp->mp check; also fixes the stale struct mount * in the devfs gate.

Verdict

hammer2_cleanup_devvp() unconditionally clears v_rdev->si_mountpoint on every device list it processes. On the routine secondary-PFS mount path (hammer2_vfsops.c:1360, runs even when the secondary mount subsequently FAILS) it strips the marker from devices backing live hammer2 mounts; on the vfs_mountedon() EBUSY path (hammer2_vfsops.c:1096) it strips the marker from devices owned by other filesystems (ufs/msdos/ext2/cd9660/ntfs/hpfs/udf). si_mountpoint is the only state behind vfs_mountedon() (double-mount guard) and the devfs raw-write gate (devfs_vnops.c:1007-1022): after the trigger, open(O_WRONLY) of the mounted raw device succeeds for any credential with node write access β€” proven on the guest by dd returning 'Device busy' before and succeeding after both trigger variants (hammer2-live and UFS-live devices), including a 200MB full-device zeroing under two live RW mounts. The marker lifecycle also parks the first mount's struct mount *, leaving a stale-pointer dereference in the devfs gate after that mount unmounts. HAMMER1 performs the correct ownership check (hammer_ondisk.c:362-366); hammer2 does not. Not a direct uid=0 chain: a protection removal that makes mounted-filesystem media attacker-writable (crafted-metadata memory-corruption surface, softdep corruption, fs destruction).