umount -f of a same-clid multi-device hammer2 cluster hangs in hammer2_thr_wait (h2twait) on unmodified images
| Field | Value |
|---|---|
| ID | DF-2631 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-667 Improper Lock Ordering / CWE-833 Deadlock |
| File | sys/vfs/hammer2/hammer2_admin.c |
| Lines | 461-469 (cleanup) / hammer2_thr_wait call sites |
| 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
Forcing an unmount (umount -f) of a hammer2 PFS assembled from multiple
devices carrying the same pfs_clid (e.g. a MASTER cloned to a second
device, or MASTER+SLAVE), hammer2_xop_helper_cleanup deletes the xop
thread columns but the teardown of the second chain's workers never
completes: umount -f ends D-state in hammer2_thr_wait (wchan
h2twait) forever. Verified on completely unmodified images
(MASTER+MASTER clone pair, no forged bytes) with the DF-2620 clamp
provably inert β i.e. this is a pre-existing multi-chain teardown defect
independent of DF-2620's bounds bug.
Root cause
hammer2_xop_helper_cleanup (hammer2_admin.c:461-469) deletes threads
per cluster index then kfrees xop_groups; the second chain's
hammer2_primary_xops_thread workers do not observe termination before
the wait gives up (or the wait is skipped/ordered wrongly for chained
pmp teardown during umount -f), leaving hammer2_thr_wait spinning in
h2twait. Exact interleaving under investigation; observed signature is
deterministic across runs.
Threat model & preconditions
- Attacker position: administrative (
umount -frequires root); severity capped accordingly, but the wedge strands kernel threads and makes the cluster's devices un-reusable until reboot. - Impact: permanent D-state hang of the umounting thread + stranded xop workers β local DoS requiring reboot.
- Reachability: create a same-clid two-device cluster (documented
hammer2 cluster setup, no crafted data), mount both,
umount -f.
Proof of concept
Control run captured during DF-2620 verification (evidence in
findings/poc/DF-2620/fixC_run.log and the control notes in
verdict.json): on the patched kernel (DF-2620 fix inert for a clean
pair) an unmodified MASTER+MASTER clone pair mounted and force-unmounted
hangs in h2twait identically to the forged variant C.
Expected output
umount -f never returns; procstat shows wchan h2twait for the umounting thread and surviving h2xop workers.
Impact
Forced-unmount path hangs permanently on legitimate multi-device clusters; reboot required.
Recommended fix
Audit the termination handshake in hammer2_xop_helper_cleanup /
hammer2_thr_delete / hammer2_thr_wait for the multi-chain case:
signal ALL cluster columns, drain xopq, and only then kfree
xop_groups (the DF-2620 fix.diff already iterates all columns; the
remaining hang is in the wait/drain ordering β likely
hammer2_thr_wait returning before remote workers exit their run loop).
References
- sys/vfs/hammer2/hammer2_admin.c:461-469, hammer2_thr_* wait/signal
- findings/poc/DF-2620/fixC_run.log (control evidence)
- DF-2620, DF-2630 (same subsystem)
Timeline
- 2026-08-28 Discovered during DF-2620 PoC verification (pass 2, GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2631 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| trigger_df2631.sh | β | 2.8 KB | view raw | |
| fix_validate.sh | β | 2.5 KB | view raw | |
| build.sh | β | 992 B | view raw | |
| run.sh | β | 992 B | view raw | |
| run.log | β | 3.4 KB | view raw | |
| fix_run.log | β | 1.3 KB | view raw | |
| build.log | β | 1.1 KB | view raw | |
| shutdown_serial.log | β | 15.7 KB | view raw | |
| fix_iteration1_panic_serial.log | β | 30.3 KB | view raw | |
| fix_iteration2_panic_serial.log | β | 29.1 KB | view raw | |
| fix_iteration3_panic_serial.log | β | 29.8 KB | view raw | |
| env.txt | β | 1.3 KB | view raw | |
| fix.diff | β | 6.2 KB | view raw | |
| manifest.json | β | 1.7 KB | view raw | |
| verdict.json | β | 5.6 KB | view raw | |
| fix.combined.diff | β | 7.7 KB | view raw |
DF-2631 β umount -f of a same-clid multi-device hammer2 cluster hangs (h2twait)
Medium / local DoS (plus an exposed pre-existing teardown UAF β see
VERDICT.md "FIX STATUS"). Two dd clones of one newfs_hammer2 image
(MASTER+MASTER, same pfs_clid), mount one, attempt the second (EBUSY,
chain merges), then umount -f β the umount thread wedges forever in
hammer2_thr_wait (wchan h2twait), hammer2_mntlk stays held (later
hammer2 mounts block in mntlk), and shutdown hangs.
Reproduce (guest, root; unmodified images)
sh trigger_df2631.sh # census before/after; leaves guest wedged
Expected (stock): umount -f pid D-state wchan h2twait; both
h2nod-testvol sync threads wchan h2coll; ~108 h2xop-testvol
survivors (frozen h2twait / mixed h2idle generations); third mount
mount_hammer2 D-state wchan mntlk; serial: "init: some processes
would not die", vn0 unmount fails BUSY. Recovery: reboot only.
Fix validation (kernel rebuilt with fix.diff)
sh fix_validate.sh # cluster umount -f must COMPLETE (bounded)
Expected (fixed, defaults): UMOUNTF_RC=0 after ~61 s
(ELAPSED=61s; with vfs.hammer2.xop_collect_timeout=10 it is ~11 s);
third mount/ls/umount RC=0; single-device sanity RC=0.
Fix status: inconclusive β the wedge is eliminated, but the
unwound teardown reproducibly triggers a delayed (1-5 min) page fault
in hammer2_primary_xops_thread (pre-existing DF-2620-family UAF that
stock never reaches because it hangs first). See VERDICT.md.
Files
trigger_df2631.sh stock-kernel wedge run + censuses
fix_validate.sh fix-kernel validation battery
run.log stock trigger output (reconstructed verbatim from
the session; decisive lines only β see note inside)
fix_run.log fix validation output (kernel #1, 61 s run +
earlier 11 s run), reconstructed from session
shutdown_serial.log degraded-shutdown serial console (raw)
fix_iteration{1,2,3}_panic_serial.log serial of the delayed panics on
intermediate fix builds (root-cause artifacts)
fix.diff freeze reorder + teardown guard + bounded collect (per-finding, applies to pristine)
fix.combined.diff BOTH findings' fixes in one diff = the exact tree built
and validated in the guest (use this to reproduce
the fix kernel; per-finding diffs touch adjacent
lines in hammer2.h/vfsops.c, so apply the combined
one or use `patch -l` when stacking them)
env.txt kernels, sysctls, image md5s
manifest.json machine-readable catalog
verdict.json machine verdict
DF-2631 β umount -f of a same-clid multi-device hammer2 cluster hangs in hammer2_thr_wait ("h2twait") on unmodified images ======================================================================= sys/vfs/hammer2/hammer2_admin.c (hammer2_thr_wait / xop machinery) + sys/vfs/hammer2/hammer2_vfsops.c (pfsfree_scan / vfs_unmount teardown)
CLAIM VERIFIED (reproduced) on the stock INVARIANTS kernel with UNMODIFIED images (dd clones of one newfs_hammer2 image, MASTER+MASTER, same pfs_clid). Fix validation: the wedge is ELIMINATED on the rebuilt kernel (umount -f completes, bounded), but a residual pre-existing UAF in the multi-chain teardown becomes reachable β see "Fix status" below.
Reproduction (stock kernel #0): 1. m1.img/m2.img = dd clones of one newfs_hammer2 -L testvol image (identical md5 956fc18a70f6689a8669e21d372b4eb9). 2. mount -o ro /dev/vn0@testvol /mnt/h2 -> RC=0 (36 h2xop workers, 0 sync threads: single master). 3. mount -o ro /dev/vn1@testvol /mnt/h2b -> EBUSY ("PFS already mounted!") BUT the second chain merged into the same pmp at clindex 1: 108 h2xop-testvol workers + 2 h2nod-testvol sync threads now exist; nmasters = 2 (count bump, vfsops.c:536-542). 4. ls /mnt/h2 -> works (quorum 2/2). 5. umount -f /mnt/h2 -> D-state, wchan "h2twait", FOREVER (observed at +3s, +8s, +63s; no progress across 60-second tsleep cycles).
Thread census at the wedge (stock): - umount -f pid 932: D3 h2twait (hammer2_thr_freeze waiting for a sync thread to reach FROZEN) - 2 x h2nod-testvol: h2coll (sync threads parked in hammer2_xop_collect, EINPROGRESS loop, no failure path) - 108 x h2xop-testvol: h2twait (frozen workers inside thr_wait_any(UNFREEZE|STOP, 0)) β plus a second mixed generation (some h2idle): xop_groups was lazily RE-CREATED mid-teardown. - third mount (vn2): mount_hammer2 D3 mntlk (hammer2_mntlk held by the wedged umount syscall). - shutdown -r: "init: some processes would not die", vn0 unmount fails BUSY, forced unmounts (shutdown_serial.log).
Root cause (path:line): 1. sys/vfs/hammer2/hammer2_synchro.c:411/454 β each sync thread's hammer2_sync_slaves() issues its xops with hammer2_xop_start_except(..., idx): its OWN cluster index is never queued and never feeds. For nmasters=2 the collect's quorum (cluster.c:348 nquorum = nmasters/2+1 = 2) is therefore unreachable forever (own column counted as 'umasters', admin.c:990-998 EINPROGRESS -> tsleep "h2coll" hz*60 -> retry). The sync threads of EVERY >=2-master cluster park in h2coll (observed pre-umount in the T2 census). 2. sys/vfs/hammer2/hammer2_vfsops.c:1664 hammer2_xop_helper_cleanup() deletes all xop workers while those sync threads are still alive mid-collect; surviving issuers re-create the whole xop_groups array lazily (admin.c:491-492), leaving worker threads inside arrays/scans that pfsfree_scan() is ripping apart. 3. sys/vfs/hammer2/hammer2_vfsops.c:772-793 pfsfree_scan()'s freeze phase freezes xop columns interleaved with the sync threads; a sync thread parked in a collect that can never be fed never observes FREEZE (checked only at its loop top, synchro.c:115/127), so hammer2_thr_freeze() (admin.c:288-294, hammer2_thr_wait "h2twait") blocks forever. umount holds hammer2_mntlk (vfsops.c:1640): all later hammer2 mounts block (wchan mntlk) and shutdown hangs the same way.
Fix (fix.diff) β three cooperating changes: a. hammer2_xop_collect(): elapsed-time bound (vfs.hammer2.xop_collect_timeout, default 60 s, 0 = legacy) on the EINPROGRESS wait, EXEMPTing super-root (SPMP) pmps whose sync threads legitimately run long scans. Event-counted bounds are defeated by continuous feeds from the healthy column (measured!), so the bound is elapsed-based. b. hammer2_pfsfree_scan(): freeze ORDER fixed β all sync threads are brought to FROZEN before any xop helper column is frozen (a frozen helper column starves in-progress collects issued by sync threads). c. hammer2_xop_start_except(): HAMMER2_PMPF_TEARDOWN guard β once hammer2_vfs_unmount() has started, no lazy re-creation of xop helpers; the starter instead feeds an immediate EOF+EIO for every node so the frontend collect fails cleanly in one pass. This eliminates the "cohort re-created mid-teardown" UAF source (observed as 36-thread survivor cohorts and Fatal trap 12 in hammer2_primary_xops_thread+0x2d9 on intermediate fix builds).
Fix validation (kernel #1): - Cluster umount -f COMPLETES: RC=0 in 61 s at the default timeout (11 s with the knob at 10) β versus infinite hang on stock. - Third mount of a fresh clone works (mntlk released), single-device mount/umount unaffected, no wedged threads immediately after.
FIX STATUS: INCONCLUSIVE (wedge fixed, adjacent latent UAF exposed). On every kernel carrying the working (elapsed) collect bound, a delayed destabilization followed the cluster umount -f 1-5 minutes later: Fatal trap 12 page fault at hammer2_primary_xops_thread+0x2d9 (lock xaddl %edx,0x81558(%rsi) β a worker touching freed xop/thread state) or a spontaneous reset (no dump). This is a PRE-EXISTING memory-corruption defect of the multi-chain teardown (same family as DF-2620: xop worker lifetime vs hammer2_xop_helper_cleanup/ pfsfree_scan teardown) that stock never reaches because the umount wedges first. Fix iterations v1 (pfsfree kfree of xop_groups), v2/v3/v4 (teardown guard + feed-EOF), v5/v6 (elapsed bound, SPMP exemption), v7 (teardown-gated bound β did not unwedge) are documented in fix_iteration{1,2,3}_panic_serial.log. Follow-up required: crash-dump analysis of the surviving worker cohort and a lifetime audit of hammer2_xop_start*() vs teardown; until then the DoS-wedge is traded for a visible (panic-once) corruption instead of a silent hang.
Recovery on stock: reboot only (umount -f never completes; mntlk held).
Fix verification
inconclusiveWEDGE ELIMINATED AND VALIDATED: on the fix kernel the cluster umount -f completes (RC=0, ELAPSED=61 s at defaults / 11 s with vfs.hammer2.xop_collect_timeout=10) instead of hanging forever, hammer2_mntlk is released (third mount/ls/umount RC=0) and single-device mounts are unaffected. HOWEVER fix_status=inconclusive: on every kernel carrying the working elapsed collect bound, the unwound teardown reproducibly triggered a delayed (1-5 min) Fatal trap 12 page fault in hammer2_primary_xops_thread+0x2d9 (worker touching freed xop/thread state) or a spontaneous reset - a pre-existing multi-chain-teardown UAF (DF-2620 family) that stock never reaches because the umount wedges first. The finding's DoS-wedge claim is fixed; the adjacent corruption needs its own root-cause pass (crash dump of the survivor cohort, lifetime audit of hammer2_xop_start* vs teardown).
fix_run.log (UMOUNTF_RC=0 ELAPSED=61s/11s, MOUNT3_RC=0, SANITY_RC=0); fix_iteration{1,2,3}_panic_serial.log (identical delayed-panic RIP); fix.diff
Confirmed kernel references
- sys/vfs/hammer2/hammer2_admin.c:135
- sys/vfs/hammer2/hammer2_admin.c:491
- sys/vfs/hammer2/hammer2_admin.c:988
- sys/vfs/hammer2/hammer2_admin.c:1148
- sys/vfs/hammer2/hammer2_vfsops.c:766
- sys/vfs/hammer2/hammer2_vfsops.c:772
- sys/vfs/hammer2/hammer2_vfsops.c:783
- sys/vfs/hammer2/hammer2_vfsops.c:1664
- sys/vfs/hammer2/hammer2_synchro.c:411
- sys/vfs/hammer2/hammer2_cluster.c:348
Detail
Evidence (decisive lines)
run.log (T1=36->T2=108 workers +2 sync threads; umount pid 932 D3 h2twait at +3/+8/+63s; h2nod-testvol h2coll; SURV63_h2xop=109; mixed h2twait/h2idle worker generations = re-created cohort); third mount mount_hammer2 D3 mntlk; shutdown_serial.log (would-not-die + BUSY + forced unmounts); fix_run.log (UMOUNTF_RC=0 ELAPSED=61s defaults / 11s at knob 10, MOUNT3_*_RC=0, SANITY_*_RC=0); fix_iteration{1,2,3}_panic_serial.log (delayed Fatal trap 12, hammer2_primary_xops_thread+0x2d9)
PoC changes
Standalone trigger written from scratch (unmodified dd-clone images, no forging); census instrumentation (thread counts, wchan histograms at +3s/+8s/+63s, third-mount mntlk probe, shutdown serial capture).
Verified recommended fix
Freeze sync threads to completion BEFORE freezing any xop helper column in pfsfree_scan; never lazily re-create xop helpers once HAMMER2_PMPF_TEARDOWN is set (feed EOF+EIO instead); bound hammer2_xop_collect's EINPROGRESS wait with an elapsed-time sysctl (vfs.hammer2.xop_collect_timeout, SPMP-exempt) - see fix.diff; follow-up required for the residual teardown UAF (crash-dump the surviving worker cohort).
Verdict
VERIFIED on stock INVARIANTS kernel with UNMODIFIED images (dd clones, MASTER+MASTER, same pfs_clid): after mounting one clone and merging the second via the expected-EBUSY mount, umount -f wedges FOREVER in hammer2_thr_wait (umount thread D-state wchan h2twait at +3s/+8s/+63s) while both h2nod-testvol sync threads sit in hammer2_xop_collect (wchan h2coll - their start_except() xops can never reach quorum because their own cluster index never feeds) and ~108 h2xop workers survive frozen (plus a second lazily re-created cohort); hammer2_mntlk stays held so a third hammer2 mount blocks D-state in mntlk, and shutdown is degraded (init: some processes would not die; vn0 unmount fails BUSY). Reboot is the only recovery. Root cause precisely localized: (1) sync threads' self-excluded collects starve forever (synchro.c:411/454 + cluster.c:348 + admin.c:990-998, no failure path); (2) xop_helper_cleanup (vfsops.c:1664) deletes workers under still-running sync threads whose xop_start_except then lazily re-creates xop_groups mid-teardown (admin.c:491-492); (3) pfsfree_scan's freeze phase (vfsops.c:772-793) starves in-progress collects by freezing helper columns, so thr_freeze never returns. Fix validation on rebuilt kernel #1: the wedge is ELIMINATED - umount -f returns RC=0 in 61 s at defaults (11 s with the knob at 10), third mount/ls/umount work, single-device mounts unaffected - but a residual PRE-EXISTING UAF of the multi-chain teardown (DF-2620 family: workers touching freed xop/thread state) reproducibly crashes the box 1-5 minutes after the cluster umount (Fatal trap 12 at hammer2_primary_xops_thread+0x2d9, identical RIP across iterations), a path stock never reaches because it hangs first; fix_status is therefore inconclusive pending crash-dump root-causing of the surviving worker cohort.
No comments yet.