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 withvfs.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.
Recommended fix
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
- sys/vfs/hammer2/hammer2_vfsops.c:1966-2008, :527-529
- findings/poc/DF-2620/ (boot_after_A.log, C_run.log)
- DF-2620 (companion unclamped-ingestion defect)
Timeline
- 2026-08-28 Discovered during DF-2620 PoC verification (pass 2, GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2630 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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):
- 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.
- First
ls /mnt/h2wedges 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). - Later lookups degenerate into UNKILLABLE D-state: after
umount -ftakes the mount exclusively busy and then itself wedges, every new crossing lookup blocks invfs_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 (orphanedls) in D-state wchanvfs_busyfor 40+ MINUTES, immune to kill -9. umount /mnt/h2(plain) wedges: sys_unmount() nlookups the path (sys/kern/vfs_syscalls.c:635-638) -> crossing -> VFS_ROOT/wedge; observed D-state wchanvfs_busy.umount -f /mnt/h2wedges in hammer2 teardown (D-state, wchanh2twait) - the wedged VFS_ROOT loop keeps re-issuing ipcluster xops every second, and the teardown waits never complete.- Even a bare
mountlisting hangs (prmount -> mountctl(f_mntonname) -> nlookup -> VFS_ROOT): accidentally proven in the first run (mount(8) pid 940, wchan h2root). - 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.
- 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).
- 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
fixedOn 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
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).
No comments yet.