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)
PoC verification
Evidence pack
findings/poc/DF-5799 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- Routine secondary PFS mount β
hammer2_vfs_mount()matches the device to an existing live hmp and then callshammer2_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 theirsi_mountpointmarker. 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. - 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 callshammer2_cleanup_devvp(&devvpl)on devices whosesi_mountpointwas 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): anyopen(O_WRONLY)of a mounted block device returnsEBUSYunless the mount is read-only (uid-independent; onlyFSCREDbypasses). 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-4267dereferencessi_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).
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:691parks the first mount'sstruct 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:1019readssi_mountpoint->mnt_flag) β a use-after-free read ofstruct 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):
- vnconfig a 200 MB file as
/dev/vn0;newfs_hammer2 -L ROOT. mount -t hammer2 /dev/vn0@ROOT /mnt/aβ rc=0.dd if=/dev/zero of=/dev/vn0 bs=64k count=1β "Device busy" (devfs gate active;si_mountpointset byhammer2_init_volumes).mount -t hammer2 /dev/vn0@LOCAL /mnt/bβ rc=0 (routine secondary PFS mount). Console showshmp=β¦ matchedthenpmp=β¦β thehammer2_cleanup_devvp(&devvpl)at vfsops.c:1360 has already run.- Same
ddβ succeeds (65536 bytes). The write protection of a device backing TWO live read-write hammer2 mounts is gone. umount /mnt/b;mount -t hammer2 /dev/vn0@NOSUCH /mnt/bβ fails ("not found") β yet the sameddstill succeeds: even a failed secondary mount attempt strips the gate (cleanup precedes the label lookup).- Cross-fs variant: UFS on
/dev/vn1(mount -t ufsrc=0);dd of=/dev/vn1β Device busy. 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).- 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 independentvcount(devvp)guard atsys/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.usermountuser 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 inhammer2_mount_helperandhammer2_pfsalloc),hammer2_devvp_unmount(hmp, mp)(ownership-checked clear on last unmount),hammer2_devvp_unmount_all(hmp)(final device teardown), plus marker cleanup inhammer2_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_volumepanic: killed βhammer2_verify_volumes_2(hammer2_ondisk.c:390-405) forcesvolu_loff[i]==-1fori >= nvolumes, andverify_commonrejectsoffset == -1, so ids are provably dense{0..nvolumes-1}. - unbounded
bzero(f_mntfromname + size, MNAMELEN - size)athammer2_vfsops.c:1497-1499on copyinstr fault: killed β DFlystd_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 βipis initialized byhammer2_inode_lookup()athammer2_vfsops.c:1915.!hmp->devvpteardown of zero-initialized vchain/fchain (hammer2_vfsops.c:1161-1167): unreachable βhammer2_init_volumesreturns EINVAL ("root volume not found") whenever*rootvoldevvpunset.statfs/statvfsleaving*sbpunwritten whennchains == 0(hammer2_vfsops.c:2047-2081): unreachable while a pmp is mounted (mount requires β₯1 chain; thepfs_nmasters > nchainswedge is DF-2630).hammer2_remountiteratinghmp->nvolumesvs sparsehmp->volumes[]: same density proof as above kills it.hammer2_pfsdeallocnot clearingiroot->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
fixedfix.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
Confirmed kernel references
- sys/vfs/hammer2/hammer2_ondisk.c:216
- sys/vfs/hammer2/hammer2_ondisk.c:225
- sys/vfs/hammer2/hammer2_ondisk.c:691
- sys/vfs/hammer2/hammer2_vfsops.c:1092
- sys/vfs/hammer2/hammer2_vfsops.c:1096
- sys/vfs/hammer2/hammer2_vfsops.c:1358
- sys/kern/vfs_subr.c:1887
- sys/vfs/devfs/devfs_vnops.c:1007
- sys/vfs/devfs/devfs_vnops.c:1018
- sys/vfs/hammer/hammer_ondisk.c:362
- sys/vfs/ufs/ffs_softdep.c:4266
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/
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).
No comments yet.