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

Sync-thread xop quorum counts the excluded node: hammer2_xop_collect() can never reach quorum, permanently parking every sync thread (h2coll) β€” precise fixable root cause of the DF-2631 umount -f wedge family

Field Value
ID DF-2657
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 / CWE-834 (unbounded wait on an eternally-in-progress vote)
File sys/vfs/hammer2/hammer2_synchro.c
Lines 411, 454, 672 (start_except), 421; sinks cluster.c:348,462-465, admin.c:990-998
Area vfs
Confidence certain
Discovered 2026-08-29
Pass 2 (GLM 5.3 second pass)
Bucket hammer2
Reported pending
Known CVE none
CVE match novel

Summary

hammer2_sync_slaves() issues all three of its xops with hammer2_xop_start_except(..., idx) so its own cluster index is never queued and can never feed, but hammer2_cluster_check() computes quorum over ALL pfs_nmasters masters (cluster.c:348) and counts the excluded element as an in-progress master (++umasters, cluster.c:462-465); with nmasters+umasters >= nquorum (cluster.c:522-527) the collect returns EINPROGRESS forever (admin.c:990-998, tsleep 'h2coll' hz*60 loop with no thr_break/STOP/FREEZE check). Affected geometries: every 2-node MASTER+MASTER cluster, MASTER(idx)+SLAVE, and every 1-element non-MASTER pmp (the boot 'LOCAL' PFS). hammer2 slave/master resynchronization has never functioned on these clusters β€” the threads park before their first comparison β€” and umount -f then stalls in hammer2_thr_freeze/h2twait and fails, leaving stranded threads, frozen xop cohorts, and hammer2_mntlk held (later hammer2 mounts D-state).

Proof of concept

VERIFIED on stock (unmodified dd-clone images): 4Γ— h2nod threads parked in h2coll pre-umount; umount -f D4 h2twait ~65s β†’ rc=1; third mount D3 mntlk; recovery reboot-only. Wedge ELIMINATED on a rebuilt kernel carrying the 10-hunk fix.diff (participant-aware quorum via HAMMER2_XOP_PARTQUORUM/chk_mask + forward-only gate want_update = mtid < sync_tid): h2idle parks, umount -f RC=0 in 0s, third mount instant, 6/6 single-device churn control clean, diverged rw cluster tears down cleanly with no regression of the fresher master. Residual: post-teardown mount churn trips the pre-existing DF-2620-family UAF at hammer2_primary_xops_thread+0x2d9 (4 traps; next defect this fix uncovers). Evidence: findings/poc/DF-2657/.

fix.diff (validated in-guest): add HAMMER2_XOP_PARTQUORUM set by hammer2_xop_start_except, track chk_mask of actual participants, pass a pmask into hammer2_cluster_check() to compute nquorum over participants only and skip non-participants in the umasters loop; plus the forward-only gate want_update = (chain->bref.modify_tid < sync_tid) so unwedged diverged clusters cannot ping-pong destructively. Full diff in the pack.

References

  • DF-2631 (the wedge this explains and fixes), DF-2620/DF-0823/DF-2654 (the residual survivor-UAF family), DF-2630

Timeline

  • 2026-08-29 Discovered during pass-2 audit of hammer2_synchro.c (GLM 5.3); reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2657 Β· 15 files
FileTypeDescriptionSize
README.md β€” 4.6 KB ↓ raw
VERDICT.md β€” 6.8 KB ↓ raw
run.log β€” 3.4 KB view raw
fix_run.log β€” 4.5 KB view raw
fix_run.truncated.log β€” 808 B view raw
trigger_df2657.sh β€” 2.7 KB view raw
fix_validate.sh β€” 4.1 KB view raw
fix.diff β€” 5.8 KB view raw
fix2657.diff β€” 5.8 KB view raw
fix_build.log.tgz β€” 135.4 KB ↓ download
panic_serial.log β€” 3.7 KB view raw
code_hashes.txt β€” 304 B view raw
guest_fix_artifacts.tgz β€” 514.8 KB ↓ download
verdict.json β€” 5.3 KB view raw
manifest.json β€” 2.2 KB view raw

DF-2657 β€” hammer2 sync-thread xop quorum counts the excluded node: every

h2nod collect can never reach quorum (root cause of the DF-2631 wedge family)

sys/vfs/hammer2/hammer2_synchro.c (hammer2_sync_slaves, lines 411/454/672 β€” the hammer2_xop_start_except(..., idx) call sites) interacting with sys/vfs/hammer2/hammer2_cluster.c:348/462-465/522-527 (quorum math) and sys/vfs/hammer2/hammer2_admin.c:888-998 (hammer2_xop_collect).

Medium / local DoS (same impact class as DF-2631: wedged sync threads, stalled/failed umount -f, mntlk held, frozen thread cohorts, shutdown degradation). Bucket: hammer2. This is the precise, fixable root cause that DF-2631 (fix: inconclusive) asked for, plus the missing direction gate that any unwedging fix needs to avoid destructive regression.

Root cause (path:line)

  • hammer2_sync_slaves() issues all three of its xops with hammer2_xop_start_except(&xop->head, desc, idx) β€” its own cluster index is never queued and can never feed (synchro.c:411, 454, 672).
  • hammer2_cluster_check() computes the quorum over the whole cluster: nquorum = pmp->pfs_nmasters / 2 + 1 (cluster.c:348), and in pass 2 the excluded element (chain==NULL, no CITEM_NULL) is counted as an "in-progress master": ++umasters (cluster.c:462-465).
  • With nmasters + umasters >= nquorum (cluster.c:523) the check returns EINPROGRESS forever β€” the excluded node's vote is awaited eternally.
  • 2-node MASTER+MASTER (nmasters=2, nquorum=2, participants=1): 1 collected + 1 phantom = 2 >= 2 β†’ EINPROGRESS forever.
  • MASTER+SLAVE where idx is the master (nquorum=1, participants have 0 masters): 0 + 1 >= 1 β†’ EINPROGRESS forever.
  • 1-element non-MASTER pmppfs (the boot "LOCAL" PFS on every mounted device): 0 participants β†’ 0 + 1 >= 1 β†’ EINPROGRESS forever.
  • hammer2_xop_collect() then loops forever (tsleep "h2coll" hz*60, admin.c:990-998) with no hammer2_thr_break()/STOP/FREEZE check inside the loop, so the sync thread can never be stopped or frozen cleanly β†’ hammer2_thr_freeze()/hammer2_thr_delete() block in h2twait (admin.c:148/259) β†’ the DF-2631 umount -f wedge + frozen cohorts.

Net effect: hammer2 slave/master resynchronization has never functioned on any cluster whose participating masters are fewer than pfs_nmasters/2+1 β€” every such sync thread parks in h2coll before its first comparison.

Fix (fix.diff, validated in-guest)

Participant-aware quorum + forward-only sync gate:

  1. hammer2_xop_start_except() marks the xop HAMMER2_XOP_PARTQUORUM (notidx >= 0) and rebuilds xop->chk_mask from scratch (= participants).
  2. hammer2_xop_collect() passes the participant mask to hammer2_cluster_check().
  3. hammer2_cluster_check() computes nquorum from participating MASTER/SUPROOT elements only and skips non-participants in pass 2 (they can neither vote nor be "in progress"). Normal all-node xops keep bit-for-bit legacy behavior (pmask = ~0).
  4. hammer2_sync_slaves() gate becomes forward-only: want_update = (chain->bref.modify_tid < sync_tid) (synchro.c:421) β€” without this, unwedged 2-master clusters would ping-pong destructively (each thread dragging its column toward the other's older tid).

Reproduce (guest, root)

sh trigger_df2657.sh     # stock: 4x h2nod in h2coll pre-umount;
                        # umount -f D4 h2twait 65 s -> rc=1, stranded
                        # threads; third mount D3 mntlk.

Fix validation (kernel #1, make nativekernel && make installkernel)

sh fix_validate.sh      # Phase A: h2idle parks (0x h2coll), umount -f
                        # RC=0 in 0 s, third mount instant, single-device
                        # sanity OK; control: 6x solo cycles clean.
                        # Phase B: diverged rw cluster, no regression,
                        # clean teardown.  Phase C: post-cluster-teardown
                        # mount churn trips the pre-existing DF-2620-
                        # family UAF (same +0x2d9 trap as DF-2631's
                        # builds) β€” see VERDICT.md.

Files

trigger_df2657.sh    stock baseline (reconstructed log: run.log)
fix_validate.sh      validation battery (reconstructed log: fix_run.log)
fix.diff             participant-quorum + forward-only gate (applies -p1)
fix_build.log.tgz    FULL untrimmed nativekernel+installkernel log (5.4 MB)
panic_serial.log     the 4 DF-2620-family traps on the fix kernel
fix_run.truncated.log in-guest log frozen by panic #1 (23 lines)
code_hashes.txt      sha256 of diff/scripts
VERDICT.md
↓ download raw

DF-2657 β€” participant-blind quorum parks every hammer2 sync thread (h2coll)

sys/vfs/hammer2/hammer2_synchro.c (sync xop issue sites 411/454/672) x sys/vfs/hammer2/hammer2_cluster.c (cluster_check quorum math) x sys/vfs/hammer2/hammer2_admin.c (xop_collect EINPROGRESS loop)

CLAIM VERIFIED (reproduced) on the stock INVARIANTS kernel, unmodified dd-clone images, and the wedge ELIMINATED on a rebuilt kernel carrying fix.diff (kernel #1). This finding supplies the precise fixable root cause requested by DF-2631 (whose own timeout-bound fix was inconclusive) and is filed as a distinct defect at the fix level: the quorum computation itself, not a timeout around it.

REPRODUCED (stock kernel #0) 1. m1/m2 = dd clones of one newfs_hammer2 -L testvol image (identical MD5), MASTER+MASTER, same pfs_clid. 2. mount -o ro vn0@testvol (RC=0); mount vn1@testvol -> EBUSY but the chain merges into the pmp (nmasters 1->2). 3. Census BEFORE any umount: 4 sync threads parked in h2coll (2x h2nod-testvol, 2x h2nod-LOCAL) β€” the permanent park. This is the defect itself: the collect can never reach quorum because the excluded own index is counted as an eternally-in-progress master (cluster.c:462-465) against nquorum = pfs_nmasters/2+1 (cluster.c:348). 4. umount -f: umount(8) D4 h2twait for ~65 s (one hz*60 tsleep cycle spanning freeze waits), then rc=1 with EMPTY error message; the 4 h2nod threads remain in h2coll FOREVER (census minutes later), frozen h2xop cohorts remain, the pmp is dead weight. 5. Third same-clid mount: D3 mntlk (hammer2_mntlk held by the stuck umount syscall), times out. => local DoS: any user who can induce an admin to mount a same-clid clone pair (or any genuine >=2-master / master+slave cluster, where the same math parks the sync threads from the moment of the merge) gets a wedgeable mount that cannot be unmounted cleanly.

ROOT CAUSE (path:line) * synchro.c:411/454/672 β€” hammer2_sync_slaves() issues ipcluster/ scanall xops via hammer2_xop_start_except(..., idx): own index never queued, never feeds (admin.c:556-571 skips i == notidx). * cluster.c:348 β€” nquorum = pmp->pfs_nmasters/2+1 over ALL masters. * cluster.c:462-465 β€” the excluded element (NULL chain, no CITEM_NULL) counts as ++umasters ("in progress") forever. * cluster.c:522-527 β€” nmasters + umasters >= nquorum => EINPROGRESS forever for: 2xMASTER (2>1), MASTER(idx)+SLAVE (1>0), 1-element non-MASTER pmp such as the boot "LOCAL" PFS (1>0). * admin.c:990-998 β€” collect loops on EINPROGRESS (tsleep "h2coll" hz*60) with NO thr_break/STOP/FREEZE check => the thread can never be stopped or frozen => admin.c:148/259 h2twait wedge (DF-2631).

FIX (fix.diff) β€” participant-aware quorum + forward-only gate a. hammer2.h: HAMMER2_XOP_PARTQUORUM (0x20) + cluster_check takes pmask. b. hammer2_admin.c hammer2_xop_start_except(): set PARTQUORUM when notidx >= 0; rebuild xop->chk_mask under xop_spin (exactly the queued participants; chk_mask is otherwise never reset β€” my clear also fixes stale bits surviving objcache recycling). c. hammer2_admin.c hammer2_xop_collect(): pmask = chk_mask for PARTQUORUM xops, else ~0 (legacy, bit-for-bit). d. hammer2_cluster.c hammer2_cluster_check(): nquorum from participating MASTER/SUPROOT elements (npmasters/2+1; pmp==NULL degenerate preserved); pass 2 skips non-participants. e. hammer2_synchro.c:421: want_update = (mtid < sync_tid) β€” forward- only. REQUIRED companion: with quorum now reachable among peers, a diverged peer with an OLDER root tid must not drag a newer column backward (otherwise a diverged 2-master cluster ping-pongs destroy/recreate between passes).

FIX VALIDATION (kernel #1, Sun Aug 30 04:30:11 UTC 2026) Build: cd /usr/src && make nativekernel && make installkernel -> BUILD_INSTALL_OK; full 5.4 MB log kept (fix_build.log.tgz). Apply: patch -p1 < fix.diff β€” all 10 hunks clean. Phase A (identical clones, the DF-2631 scenario): h2nod census post-merge: 4x h2idle, A_H2COLL_COUNT=0 (stock: 4x h2coll forever); ls OK (VFS quorum path unchanged); umount -f: RC=0 in 0 s (stock: rc=1 after 65 s + stranded threads); third mount: RC=0 instant (stock: mntlk D3); single-device sanity mount/umount: RC=0/RC=0. Control: 6x solo mount/ls/umount churn on one boot: all RC=0, guest up, no panics => the fix does not disturb single-device pmp lifecycle. Phase B (diverged rw cluster, m1 has X / m2 not): merge OK; 4x h2idle; no "hammer2_sync_slaves: error" or "no quorum possible" spam; plain umount RC=0 in 0 s (stock: impossible); b1 (fresher) still contains X after the session => NO REGRESSION (forward-only gate holds). Pull-in of X into the stale clone's media was NOT observed β€” see Follow-ups. Phase C (stability): any session combining a cluster merge+teardown with 1-3 subsequent same-clid mount operations panicked at hammer2_primary_xops_thread+0x2d9 (lock xaddl %edx, 0x81558(%rsi)) β€” 4 occurrences (trap 12 and trap 1). This is the PRE-EXISTING DF-2620-family multi-chain teardown UAF: identical trap site+instruction to the delayed panics on DF-2631's independent (timeout-based) fix builds, and the no-cluster control is clean. Stock never reaches it because the umount wedges first. NOT introduced by this fix; it is the next defect in the teardown chain this fix uncovers.

FIX STATUS: fixed (for the filed defect) β€” the h2coll park, the umount -f stall/failure, the mntlk hold and the frozen cohorts are all eliminated on the patched kernel; single-device and VFS-quorum behavior unchanged. Full-system stability after cluster teardown remains blocked by the separate, pre-existing DF-2620-family survivor-worker UAF (documented, out of scope here).

FOLLOW-UPS (open, honestly noted) 1. DF-2620 family: crash-dump analysis of the +0x2d9 survivor cohort after cluster teardown (xop_groups lifetime vs pfsfree_scan). 2. Stale-column pull-in: with participant quorum the stale column's sync pass completes without error yet X did not appear on m2's media (b1/b2 MD5s differ; b1 keeps X). Suspects: flush semantics of sync-thread-created chains on the second column, or the non-modifying trans flags the sync thread runs under (hammer2_trans_init(pmp, 0), synchro.c:178). Needs vfs.hammer2.debug=0x8000 tracing on a UAF-free kernel (every diagnostic attempt on this guest was killed by the DF-2620 trap).

Recovery on stock: reboot only (umount never completes cleanly).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: rebuilt kernel eliminates the wedge - h2idle parks (0 h2coll), umount -f RC=0 in 0s, third mount RC=0, 6/6 single-device churn control clean, diverged rw cluster tears down cleanly with the fresher master preserved (forward-only gate required). Residual DF-2620-family survivor UAF trips post-teardown (pre-existing, separately tracked).

fix_run.log / fix_build.log.tgz / panic_serial.log (4 residual UAF traps attributed to DF-2620 family)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1 (in-guest make nativekernel + DF-2657 fix.diff, 10 hunks; see fix_build.log.tgz)

Confirmed kernel references

Detail

Exploit chain

unprivileged-induced admin action (or any real >=2-master cluster): mount same-clid dd clones -> EBUSY merge -> all h2nod sync threads park in h2coll forever -> umount -f wedges/fails with threads stranded -> hammer2_mntlk held -> all later hammer2 mounts D-state -> shutdown degradation; recovery reboot-only. Not a privilege escalation: local DoS of the mount/teardown machinery.

Evidence (decisive lines)

run.log (stock: 4x h2coll pre-umount census, umount -f D4 h2twait +65s rc=1, third mount D3 mntlk); fix_run.log (kernel #1: A_H2COLL_COUNT=0/4x h2idle, A_UMOUNTF_RC=0 A_ELAPSED=0s, A_MOUNT3_RC=0, 6/6 solo churn clean, Phase B no-regression); fix.diff (10 hunks clean); fix_build.log.tgz (5.4MB BUILD_INSTALL_OK); panic_serial.log (4x DF-2620-family +0x2d9 traps on fix kernel, control clean); VERDICT.md (full narrative + open follow-ups)

PoC changes

Wrote fresh trigger (DF-2631 methodology adapted: fresh newfs base image since the guest snapshot was clean, vn0-based newfs, added the decisive pre-umount h2coll census and +65s post-cycle census); wrote fix_validate.sh in three phases (wedge regression / diverged-cluster / stability) and had to run phases as discrete ssh commands after panic #1 froze the in-guest log writer; fix.diff authored against pristine sys/ after code-reading cluster_check/admin/synchro.

Verified recommended fix

Participant-aware quorum: thread the xop participant mask (chk_mask) from hammer2_xop_start_except() through hammer2_xop_collect() into hammer2_cluster_check() and compute nquorum over participating masters only; pair with a forward-only want_update gate (mtid < sync_tid) in hammer2_sync_slaves().

Verdict

Verified end-to-end. On stock, every sync thread of a same-clid 2-master cluster (and of master+slave-with-idx-master clusters, and of 1-element non-MASTER pmppfs like the boot LOCAL PFS) parks permanently in hammer2_xop_collect's EINPROGRESS loop ('h2coll') because hammer2_cluster_check computes quorum over all pfs_nmasters masters while the sync thread's xop was issued with hammer2_xop_start_except() excluding its own index - the excluded element is counted as an eternally in-progress master (umasters), so nmasters+umasters >= nquorum forever. umount -f then stalls ~65s in h2twait and fails rc=1 leaving the 4 sync threads parked forever, frozen xop cohorts, and hammer2_mntlk held (third mount D-state). Root-cause fix (participant-aware quorum via chk_mask + forward-only want_update gate) rebuilt in-guest: sync threads park in h2idle (0x h2coll), umount -f completes RC=0 in 0s, third mount instant, 6x single-device churn control clean, diverged rw cluster shows no regression of the fresher master and clean teardown. Residual: post-cluster-teardown mount churn trips the pre-existing DF-2620-family UAF at hammer2_primary_xops_thread+0x2d9 (identical trap site to DF-2631's independent fix builds; no-cluster control clean) - documented, not introduced by this fix.