diff -urN a/sys/vfs/hammer2/hammer2.h b/sys/vfs/hammer2/hammer2.h --- a/sys/vfs/hammer2/hammer2.h 2026-08-30 04:20:54.789834671 +0000 +++ b/sys/vfs/hammer2/hammer2.h 2026-08-30 04:20:54.789834671 +0000 @@ -1083,6 +1083,12 @@ #define HAMMER2_XOP_INODE_STOP 0x00000004 #define HAMMER2_XOP_VOLHDR 0x00000008 #define HAMMER2_XOP_FSSYNC 0x00000010 +/* + * XOP issued via hammer2_xop_start_except(): the excluded cluster index + * never feeds the collect, so quorum must be calculated from the nodes + * the xop was actually issued to (chk_mask), not from pfs_nmasters. + */ +#define HAMMER2_XOP_PARTQUORUM 0x00000020 /* * Device vnode management structure @@ -1913,7 +1919,7 @@ void hammer2_cluster_rehold(hammer2_cluster_t *cluster); void hammer2_cluster_lock(hammer2_cluster_t *cluster, int how); int hammer2_cluster_check(hammer2_cluster_t *cluster, hammer2_key_t lokey, - int flags); + int flags, uint64_t pmask); void hammer2_cluster_unlock(hammer2_cluster_t *cluster); void hammer2_bulkfree_init(hammer2_dev_t *hmp); diff -urN a/sys/vfs/hammer2/hammer2_admin.c b/sys/vfs/hammer2/hammer2_admin.c --- a/sys/vfs/hammer2/hammer2_admin.c 2026-08-30 04:20:54.789834671 +0000 +++ b/sys/vfs/hammer2/hammer2_admin.c 2026-08-30 04:20:54.789834671 +0000 @@ -547,11 +547,20 @@ xop->desc = desc; /* + * Quorum for start_except() XOPs must be calculated from the + * participants only (the excluded index can never vote). Used by + * hammer2_xop_collect() when calling hammer2_cluster_check(). + */ + if (notidx >= 0) + xop->flags |= HAMMER2_XOP_PARTQUORUM; + + /* * The instant xop is queued another thread can pick it off. In the * case of asynchronous ops, another thread might even finish and * deallocate it. */ hammer2_spin_ex(&pmp->xop_spin); + xop->chk_mask = 0; nchains = ip1->cluster.nchains; for (i = 0; i < nchains; ++i) { /* @@ -891,6 +900,7 @@ hammer2_chain_t *chain; hammer2_key_t lokey; uint64_t mask; + uint64_t pmask; int error; int keynull; int adv; /* advance the element */ @@ -985,7 +995,20 @@ (mask & HAMMER2_XOPMASK_ALLDONE) != HAMMER2_XOPMASK_VOP) { error = HAMMER2_ERROR_EINPROGRESS; } else { - error = hammer2_cluster_check(&xop->cluster, lokey, keynull); + /* + * For XOPs issued via hammer2_xop_start_except() the quorum + * must be calculated from the participating nodes only, + * otherwise the collect can never reach quorum and will + * retry forever (this parks the sync threads of any cluster + * whose participating masters are fewer than pfs_nmasters/2+1, + * e.g. every MASTER+MASTER cluster). + */ + if (xop->flags & HAMMER2_XOP_PARTQUORUM) + pmask = xop->chk_mask; + else + pmask = ~0ULL; + error = hammer2_cluster_check(&xop->cluster, lokey, + keynull, pmask); } if (error == HAMMER2_ERROR_EINPROGRESS) { if (flags & HAMMER2_XOP_COLLECT_NOWAIT) diff -urN a/sys/vfs/hammer2/hammer2_cluster.c b/sys/vfs/hammer2/hammer2_cluster.c --- a/sys/vfs/hammer2/hammer2_cluster.c 2026-08-30 04:20:54.789834671 +0000 +++ b/sys/vfs/hammer2/hammer2_cluster.c 2026-08-30 04:20:54.789834671 +0000 @@ -318,7 +318,8 @@ * XXX needs to handle SOFT_MASTER and SOFT_SLAVE */ int -hammer2_cluster_check(hammer2_cluster_t *cluster, hammer2_key_t key, int flags) +hammer2_cluster_check(hammer2_cluster_t *cluster, hammer2_key_t key, int flags, + uint64_t pmask) { hammer2_chain_t *chain; hammer2_chain_t *focus; @@ -330,6 +331,7 @@ int ttlslaves; int nmasters; int nmasters_keymatch; + int npmasters; int nslaves; int nquorum; int umasters; /* unknown masters (still in progress) */ @@ -344,8 +346,33 @@ /* * Calculate quorum + * + * pmask contains the cluster indices the XOP was actually issued + * to (~0 for a normal all-node XOP). When a node was excluded via + * hammer2_xop_start_except() it can never vote (its element stays + * NULL and its fifo is never fed), so it must not be counted in + * the quorum nor as an in-progress master. Otherwise, for any + * cluster whose participating masters are fewer than + * pfs_nmasters/2+1 (e.g. every 2-node MASTER+MASTER cluster, where + * the quorum of 2 can never be reached by the single remaining + * participant), the check returns EINPROGRESS forever. */ - nquorum = pmp ? pmp->pfs_nmasters / 2 + 1 : 0; + npmasters = 0; + if (pmp) { + for (i = 0; i < cluster->nchains; ++i) { + if ((pmask & (1LLU << i)) == 0) + continue; + switch (pmp->pfs_types[i]) { + case HAMMER2_PFSTYPE_MASTER: + case HAMMER2_PFSTYPE_SUPROOT: + ++npmasters; + break; + default: + break; + } + } + } + nquorum = pmp ? npmasters / 2 + 1 : 0; nflags = 0; ttlmasters = 0; ttlslaves = 0; @@ -452,6 +479,14 @@ continue; } + /* + * Non-participating nodes (the excluded index of a + * hammer2_xop_start_except() XOP) can never vote and + * must not be counted as in-progress masters. + */ + if ((pmask & (1LLU << i)) == 0) + continue; + chain = cluster->array[i].chain; error = cluster->array[i].error; diff -urN a/sys/vfs/hammer2/hammer2_synchro.c b/sys/vfs/hammer2/hammer2_synchro.c --- a/sys/vfs/hammer2/hammer2_synchro.c 2026-08-30 04:20:54.789834671 +0000 +++ b/sys/vfs/hammer2/hammer2_synchro.c 2026-08-30 04:20:54.789834671 +0000 @@ -418,7 +418,15 @@ &parent, HAMMER2_RESOLVE_ALWAYS | HAMMER2_RESOLVE_SHARED); - want_update = (chain->bref.modify_tid != sync_tid); + /* + * Forward-only synchronization: with the quorum now + * calculated from the participating (i.e. remote) + * nodes only, a diverged peer may have an OLDER tid + * than ours. Never regress our own newer state + * towards it, or a diverged 2-master cluster will + * ping-pong destructively between passes. + */ + want_update = (chain->bref.modify_tid < sync_tid); if (chain) { hammer2_chain_unlock(chain); hammer2_chain_drop(chain);