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

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 -f requires 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.

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

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-2631 Β· 18 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

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

inconclusive
baseline reproduced→ patch + rebuild →patched clean

WEDGE 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
↓ 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 (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.