--- id: "bug-1434" status: "fixed" status_updated: "2026-09-24" status_note: "" component: "controller" severity: "unknown" timestamp: "2026-07-10T10:56:00Z" last_seen: "2026-09-19T09:00:00Z" occurrences: 1 files: ["src/controller/persist.c", "src/controller/ha.c"] tags: ["raft", "committed_lsn", "race-condition", "monotonicity", "audit-finding", "CAS"] related: ["bug-1431"] legacy: true --- # bug-1434: controller-invariant-audit (2026-07-10, /xce-audit-invariants) Raft TLA+ checklist dimension… ## Description controller-invariant-audit (2026-07-10, /xce-audit-invariants) Raft TLA+ checklist dimension: persist_promotion_advance_committed and ha_standby_advance_committed_and_apply can race on persist_t.committed_lsn during the promotion window before role flips to CTRL_PRIMARY, via an unlocked load-then-conditional-store pattern in both functions -- a losing racer could clobber a larger value with a smaller one, regressing committed_lsn (violates the documented commitIndex-monotonic invariant). ## Environment _Not recorded (migrated from buglog.json)._ ## Reproduction _Not recorded (migrated from buglog.json)._ ## Root cause raft_cb_become_leader calls persist_promotion_advance_committed BEFORE setting ha->role = CTRL_PRIMARY (the role store sits ~170 lines later in the function). During that window this node is still nominally non-primary, so ha_apply_wal_catchup_entry (still processing an in-flight WAL catch-up entry on another thread, e.g. if this node was mid-catch-up when it won the election) can legitimately call ha_standby_advance_committed_and_apply concurrently with the promotion path own committed_lsn advance. Both did atomic_load(committed_lsn); if(target<=cur) return; atomic_store(committed_lsn, target) with no lock and no CAS, a classic lost-update race under concurrent writers. ## Fix Added persist_committed_lsn_advance_atomic(persist_t*, uint64_t target) (persist.c, declared in persist.h) -- a CAS retry loop (atomic_compare_exchange_weak_explicit) that is safe against any number of concurrent callers with no lock required, and can never regress committed_lsn regardless of interleaving. Both persist_promotion_advance_committed and ha_standby_advance_committed_and_apply now route through this shared helper. Validated: build clean (19/19 unit test suites pass), 6-round chaotic soak PASS (0 loss, 0 divergence), 6-round sequential+leadership-coverage soak PASS. ## Verification _Not recorded (migrated from buglog.json)._ ## References - Related: bug-1431