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

hammer2 VFS_ROOT quorum wedge: pfs_nmasters > nchains pins mount forever and holds the namecache lock

Field Value
ID DF-2630
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-662 Improper Synchronization / CWE-833 Deadlock
File sys/vfs/hammer2/hammer2_vfsops.c
Lines 1966-2008
Area vfs
Confidence certain
Discovered 2026-08-28
Pass 2 (GLM 5.3 second pass β€” surfaced during DF-2620 PoC verification)
Bucket hammer2
Reported pending
Known CVE none
CVE match novel

Summary

When a mounted PFS's on-disk meta.pfs_nmasters exceeds the number of cluster chains actually present (nmasters > nchains, e.g. a single-device image claiming 2 masters), hammer2_vfs_root (vfsops.c:1966-2008) waits for a quorum that can never form. The wait has no timeout and no failure path: the calling thread sleeps forever (wchan h2root), and because the lookup path holds the mountpoint's namecache lock (subsequent lookups block in D-state on ncplk), the entire mount subtree becomes unusable. Path-based umount also crosses VFS_ROOT, so the mount cannot be removed normally β€” only umount -f or a reboot clears it.

Root cause

hammer2_vfs_root β†’ cluster-focus loop at vfsops.c:1966-2008 waits for nchains >= quorum with nmasters derived from the unclamped on-disk value (see DF-2620 for the ingestion site at vfsops.c:527-529). With nmasters=2, nchains=1 the quorum condition is permanently false and the loop tsleeps indefinitely (no PCATCH-style abort, no mount-ro fallback).

Threat model & preconditions

  • Attacker position: mount a crafted single-device image with meta.pfs_nmasters >= 2 (root, or unprivileged with vfs.usermount=1
  • owned device). Also reachable without any crafted bytes on a real cluster whose peers are absent at mount time.
  • Impact: permanent kernel-thread wedge + pinned namecache lock β†’ local DoS of the mount (and of any path lookup crossing it); blocks normal unmount.
  • Reachability: mount -t hammer2 ...; ls <mnt> β€” first VFS_ROOT call wedges.

Proof of concept

Observed live during DF-2620 verification (evidence in findings/poc/DF-2620/, run logs boot_after_A.log / C_run.log): mounting the forged pfs_nmasters=0xFF image succeeded, and every path-based umount/stats crossed VFS_ROOT and blocked forever in h2root β€” the DF-2620 cleanup panic had to be triggered through shutdown β†’ vfs_unmountall instead. Threads showed wchan h2root, with lookups piling up D-state on the namecache lock.

Expected output

mount OK; any stat/ls on the mountpoint never returns; procstat -W shows
h2root wchan; umount (non-forced) hangs the same way.

Impact

Deterministic local DoS: unkillable kernel threads, unusable subtree, unmountable filesystem.

Fail fast (or degrade to read-only) when the quorum cannot be met: bound the wait in hammer2_vfs_root (vfsops.c:1966-2008) β€” if nclusters < nmasters at mount completion, either refuse the mount (EINVAL) or mark the PFS degraded and return the best-focus root instead of sleeping forever; also clamp pfs_nmasters at ingestion (DF-2620 fix) so crafted values cannot manufacture the imbalance.

References

Timeline

  • 2026-08-28 Discovered during DF-2620 PoC verification (pass 2, GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2630 Β· 18 files
FileTypeDescriptionSize
README.md β€” 3.0 KB ↓ raw
VERDICT.md β€” 4.4 KB ↓ raw
forge_df2630.py β€” 5.4 KB view raw
trigger_df2630.sh β€” 2.7 KB view raw
control_df2630.sh β€” 447 B view raw
fix_validate.sh β€” 1.8 KB view raw
build.sh β€” 626 B view raw
run.sh β€” 779 B view raw
run.log β€” 27.2 KB view raw
dstate_census.txt β€” 2.1 KB view raw
shutdown_serial.log β€” 15.2 KB view raw
fix_run.log β€” 1.7 KB view raw
build.log β€” 1.0 KB view raw
env.txt β€” 1.7 KB view raw
fix.diff β€” 2.3 KB view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 5.1 KB view raw
fix.combined.diff β€” 7.7 KB view raw

DF-2630 β€” hammer2 VFS_ROOT quorum wedge (pfs_nmasters > nchains)

Medium / local DoS. A single forged byte (PFS inode meta.pfs_nmasters 0x00 -> 0x02) on a single-device hammer2 image makes every operation that crosses into the mount wedge forever in hammer2_vfs_root (wchan h2root), and β€” after umount -f wedges in teardown β€” every further lookup on the mountpoint piles up unkillable D-state (vfs_busy), the mount can never be unmounted, and shutdown is degraded. Fix (bounded + sticky-fail VFS_ROOT) validated on a rebuilt kernel.

Reproduce

1. Forge the images (host, needs python3)

# base image created once in the guest:
#   truncate -s 64M base.img && newfs_hammer2 -L testvol base.img
python3 forge_df2630.py base.img wedge   wedge.img   # nmasters=2 (wedge)
python3 forge_df2630.py base.img control control.img # nmasters=1 (control)
cmp -l wedge.img control.img     # exactly ONE byte differs (0x1400886)

2. Run in the guest (root)

sh trigger_df2630.sh        # wedge + full census (leaves guest wedged)
# separate boot:
sh control_df2630.sh        # causality control: mount/ls/umount all RC=0

Expected (stock kernel): ls /mnt/h2 child in S-state wchan h2root; later lookups + umount in D-state wchan vfs_busy (survive kill -9); umount -f in D-state wchan h2twait; serial console at shutdown: "init: some processes would not die", "unmount ... failed (BUSY)". Recovery: reboot only (umount -f does not complete on stock).

3. Fix validation (kernel rebuilt with fix.diff)

sh fix_validate.sh          # sanity + control + wedge + health + umount -f

Expected (fixed kernel, root_timeout=5 demo): ls #1 fails EIO after ~5 s; ls #2 fails instantly (sticky); zero h2root/vfs_busy/ ncplk waiters; umount -f /mnt/h2 returns 0 and removes the mount; fresh hammer2 mounts elsewhere keep working.

Files

forge_df2630.py      image forger (wedge | control) β€” host python3
trigger_df2630.sh    stock-kernel wedge run + census
control_df2630.sh    control-image causality run
fix_validate.sh      fix-kernel validation battery
run.log              stock-kernel trigger output (full, untrimmed)
dstate_census.txt    unkillable D-state pile-up (reconstructed from the
                     live session; the guest files were lost to resets)
shutdown_serial.log  serial console of the degraded shutdown (raw)
fix_run.log          fixed-kernel validation output (kernel #1, v4)
fix.diff             the validated fix (hammer2.h + hammer2_vfsops.c)
env.txt              kernels, sysctls, image md5s
manifest.json        machine-readable catalog
verdict.json         machine verdict

NOTE on applying fixes: DF-2630's and DF-2631's per-finding fix.diff files touch adjacent lines in hammer2.h and hammer2_vfsops.c; each applies cleanly to the pristine tree on its own, but stacking them sequentially needs patch -l (or apply fix.combined.diff, which is the exact tree built and validated in the guest).

VERDICT.md
↓ download raw

DF-2630 β€” hammer2 VFS_ROOT quorum wedge: pfs_nmasters > nchains pins the mount and produces an unkillable D-state pile-up ========================================================================= sys/vfs/hammer2/hammer2_vfsops.c:1966-2011 (stock tree)

CLAIM VERIFIED (reproduced) on the stock INVARIANTS kernel, and the fix VALIDATED on a rebuilt kernel (kernel #1).

What was verified (stock kernel, wedge.img = newfs image with ONE byte patched: testvol PFS inode meta.pfs_nmasters 0x00 -> 0x02; CHECK_NONE brefs + volhdr CRC recompute; control.img differs from wedge.img by exactly that one byte: 0x01):

  1. mount(2) itself RETURNS (vfsops.c:1949 hammer2_vfs_root is not called by the mount syscall; VFS_ROOT fires on the first lookup crossing). MOUNT_RC=0, same-second timestamps.
  2. First ls /mnt/h2 wedges in hammer2_vFS_root: child process in S-state, wchan "h2root" (the PCATCH tsleep at vfsops.c:2007, 1 Hz retry loop with NO timeout and NO failure path while pmp->inode_tid == 0).
  3. Later lookups degenerate into UNKILLABLE D-state: after umount -f takes the mount exclusively busy and then itself wedges, every new crossing lookup blocks in vfs_busy (nlookup crossing loop, sys/kern/vfs_nlookup.c:1053-1062; cache_resolve_mp, sys/kern/vfs_cache.c:4504-4507). Observed: pids 877/884 (orphaned ls) in D-state wchan vfs_busy for 40+ MINUTES, immune to kill -9.
  4. umount /mnt/h2 (plain) wedges: sys_unmount() nlookups the path (sys/kern/vfs_syscalls.c:635-638) -> crossing -> VFS_ROOT/wedge; observed D-state wchan vfs_busy.
  5. umount -f /mnt/h2 wedges in hammer2 teardown (D-state, wchan h2twait) - the wedged VFS_ROOT loop keeps re-issuing ipcluster xops every second, and the teardown waits never complete.
  6. Even a bare mount listing hangs (prmount -> mountctl(f_mntonname) -> nlookup -> VFS_ROOT): accidentally proven in the first run (mount(8) pid 940, wchan h2root).
  7. Shutdown is degraded: serial console shows "init: some processes would not die", "unmount of filesystem mounted from /dev/vn0@testvol failed (BUSY)", forced unmounts; the box only goes down via the boot()-time forced-unmount path. shutdown_serial.log.
  8. Causality proof: control.img (pfs_nmasters=1, one byte different) mounts, lists and unmounts perfectly (CONTROL_MOUNT_RC=0, CONTROL_LS_RC=0, CONTROL_UMOUNT_RC=0).
  9. Unprivileged reachability: vfs.usermount=0 on this guest; with usermount=1 + an owned vn/mem device the same image would be mountable by an unprivileged user (same position as DF-2620: root-position-only on this build, recorded).

Anomaly noted honestly: one early stat /mnt/h2 returned the covered directory's attributes without wedging (inum 2501377919 = hashed root-fs inum); operations that must CROSS into the mount (opendir/ readdir, umount by path, mountctl) wedge permanently.

Root cause chain (path:line): - sys/vfs/hammer2/hammer2_vfsops.c:527-528 ingests on-disk ripdata->meta.pfs_nmasters verbatim (uint8_t, no clamp, no reconciliation against the number of attached chains). - sys/vfs/hammer2/hammer2_cluster.c:348 nquorum = pfs_nmasters/2+1 = 2

nchains = 1: hammer2_cluster_check() can never reach quorum (ESRCH/EINPROGRESS forever). - sys/vfs/hammer2/hammer2_vfsops.c:1966-2011 loops forever (1 Hz tsleep "h2root"), holding the iroot lock across iterations and pinning whatever locks the caller holds (vfs_busy ref; ncp lock in the cache_resolve_mp path).

Fix (fix.diff, validated): - New sysctl vfs.hammer2.root_timeout (default 60 s, 0 = legacy infinite): hammer2_vfs_root bounds its total quorum wait and fails with EIO; a sticky HAMMER2_PMPF_ROOTFAILED flag makes subsequent VFS_ROOT calls fail immediately instead of re-waiting on every lookup. - Validated on the rebuilt kernel: wedge.img ls fails with EIO after the (5 s demo) timeout, second ls fails instantly, ZERO h2root/ vfs_busy/ncplk waiters, dmesg shows "root quorum cannot be reached (nmasters=2 nchains=1)", umount -f cleanly REMOVES the wedged mount (RC=0), further hammer2 mounts work, and the control + stock images behave identically to stock. 280-second post-run soaks on the fixed kernel were clean (no panics, no leftover threads). - Residual (documented): the bogus mount itself cannot be removed by a plain umount by path (the path lookup fails EIO once the root is marked failed), but umount -f removes it and shutdown unmounts it; the system otherwise stays fully healthy.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

On the rebuilt kernel the previously-observed bad behavior is GONE: wedge-image ls fails EIO after the (5 s demo) bound instead of wedging forever, the sticky flag makes retries instant, no h2root/vfs_busy/ncplk D-state pile forms, umount -f returns 0 and removes the bogus mount, and stock/control images are unaffected (mount/ls/umount all RC=0). 280 s post-run soaks clean, zero leftover threads, no panics. The 2630 hunks are byte-identical across all validated fix builds.

fix_run.log (SANITY/CONTROL RC=0, WEDGE_LS1_RC=1 after exactly 5 s with 'Input/output error', WEDGE_LS2_RC=1 same-second sticky, CENSUS_DONE zero waiters, dmesg quorum message, WEDGE_UMOUNTF_RC=0, HEALTH_MOUNT_RC=0); fix.diff
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sat Aug 29 05:10:55 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (steps 1-10 incl. MOUNT_RC=0 same-second, ls child wchan h2root, kill -9 survival, umount/umount -f wedges); dstate_census.txt (7 D-state processes wchan vfs_busy/h2twait 40+ min after kill -9); shutdown_serial.log (init: some processes would not die; unmount of /dev/vn0@testvol failed (BUSY); forced unmounts); fix_run.log (WEDGE_LS1 EIO after exactly 5 s, sticky instant fail, WEDGE_UMOUNTF_RC=0, dmesg 'root quorum cannot be reached (nmasters=2 nchains=1)'); control run CONTROL_*_RC=0

PoC changes

Clean standalone forger derived from DF-2620's forge (same CHECK_NONE/CRC technique) with a minimal realistic nmasters=2 variant plus a one-byte-different nmasters=1 control; trigger script hardened after the first run (bare mount replaced by mount -p because prmount->mountctl->nlookup itself wedges on the mountpoint - accidentally proven, mount(8) pid 940 wchan h2root).

Verified recommended fix

Bound hammer2_vfs_root's quorum wait with sysctl vfs.hammer2.root_timeout (default 60 s, 0=legacy) and fail VFS_ROOT with EIO, remembering the failure in HAMMER2_PMPF_ROOTFAILED so subsequent lookups fail immediately; see fix.diff.

Verdict

VERIFIED on stock INVARIANTS kernel: a single forged byte (PFS inode meta.pfs_nmasters 0x00->0x02, minimal realistic value, CHECK_NONE brefs + volhdr CRC recompute) on a single-device hammer2 image makes every mount-crossing operation wedge forever in hammer2_vfs_root (wchan h2root, 1 Hz PCATCH retry loop with no timeout/failure path at vfsops.c:1966-2011 because nquorum=2>nchains=1 can never be met); the first wedged lookups are signal-killable, but once umount -f takes the mount exclusively busy and itself wedges in teardown (h2twait), every further lookup piles up UNKILLABLE D-state in vfs_busy (observed 40+ minutes, kill -9 immune, pids 877/884), plain umount-by-path wedges in sys_unmount's nlookup, even a bare mount listing wedges via mountctl->nlookup, and shutdown is degraded ('init: some processes would not die', vn0 unmount fails BUSY, forced unmounts) - reboot is the only recovery. Causality proven by the one-byte-different control image (nmasters=1): mount/ls/umount all RC=0. Fix validated on rebuilt kernel #1: bounded (sysctl vfs.hammer2.root_timeout, default 60 s, 0=legacy) + sticky HAMMER2_PMPF_ROOTFAILED failure makes ls fail with EIO after the bound, retries fail instantly, zero h2root/vfs_busy waiters remain, umount -f cleanly REMOVES the bogus mount, further mounts work, control and stock images behave identically, and 280 s post-run soaks were clean. Residual documented: plain umount-by-path of the bogus mount fails EIO (path lookup fails once the root is marked failed) - umount -f removes it. vfs.usermount=0 on this guest, so mounts were root-position (same caveat as DF-2620).