Skip to content

rep: __rep_cmp_vote must be a total order (election can elect older data, data loss) - #266

Merged
gburd merged 4 commits into
masterfrom
fix/rep-elect-datagen
Nov 4, 2026
Merged

gburd merged 4 commits into
masterfrom
fix/rep-elect-datagen

Conversation

@gburd

@gburd gburd commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

E1: __rep_cmp_vote must be a total order (election can elect older data)

The election winner comparator __rep_cmp_vote (src/rep/rep_elect.c) was not
a consistent order, with two defects (CBMC H1 finding on #263, "Counterexample
1" and "Counterexample 2"):

  1. First-vote special case — the first tallied vote (rep->sites == 1) was
    adopted outright, so the winner depended on arrival order.
  2. data_gen bypass — the last arm (cmp == 0 && (priority/tiebreaker …))
    fired whenever the LSNs were equal regardless of data_gen, so a vote
    with an OLDER data_gen but an equal LSN and a higher priority or tiebreaker
    could beat a vote with a NEWER data_gen.

Defect 2 is a durability bug, not only liveness. Electing a site with older
data rolls back the newer site's tail on sync — a client memcmps the log
bytes at its verify LSN (__rep_verify, rep_verify.c:77), so a wrong winner
truncates the loser's log and loses a commit that was already durable.

What is proven vs argued

  • Proven: the comparator was order-dependent (CBMC + the C gate fail on the
    old code); and the precondition is reachable on real sites — two/three
    electable sites with EQUAL last-commit LSN and DIFFERENT data_gen, carrying
    those values in real VOTE1 messages, reproduced against the live engine
    (dev box). A VOTE1's LSN is the last __txn_regop and its data_gen is the
    highest REPLSNHIST gen; these are independent, so equal LSN + different
    data_gen is reachable, and no invariant forbids it.
  • Argued: that a real network delivers the stale VOTE1 last at a majority
    (an ordinary, non-zero-probability event) so the stale site is actually
    elected. Full write-up and the mixed-version rolling-upgrade analysis +
    user-facing upgrade note are in test/rep/E1-FINDINGS.md.

The fix

__rep_cmp_vote is rewritten as a single total order applied uniformly to
every vote including the first (keyed on rep->sites == 1, since rep->winner
is not reset between elections): electable > not; non-zero priority > zero
(mixed-version ELECTABLE preserved); then data_gen, LSN, priority,
tiebreaker, and finally eid as the always-distinct discriminator so the
relation is total even if two sites draw equal random tiebreakers.

No format / ABI / region change.

Tests

  • test/db/run_rep_cmpvote_order.sh — the deterministic gate: drives a
    verbatim copy of the real comparator (drift-checked by
    check_rep_cmpvote_copy.sh) over the captured real vote vectors in every
    arrival order. PASSES on the fix, FAILS with -DCMPVOTE_OLD (the 2-site
    stale-vs-newer case: arrival order flips the winner between the newer and the
    stale site). In test/MANIFEST (db tier), so it runs under run_all.sh.
  • test/tcl/rep200.tcl — ADVISORY: reproduces the precondition (three
    electable sites, equal LSN, data_gen 2/3/3) against the real engine every
    run. Its header explains why a deterministic end-to-end TCL election gate is
    not possible here (egen vs data_gen coupling in the harness) and points to
    the comparator-level gate.
  • test/cbmc/harness_cmpvote.c update (see below) proves (a) order
    independence and (e) data_gen-maximality on the fix; the old code is the
    sabotage arm and fails both.

Regressions (dev box, non-diagnostic --enable-test --enable-tcl)

rep001–rep020 and the election set (rep002/005/016/020/022/026/063/067/069/ 076/093/094) all PASS on both master-baseline and the fix, identical — no
regression.

Note: the --enable-diagnostic rep election tests panic on a pre-existing
RFC 0010 A3 lock-order artifact in src/rep/rep_backup.c (the TCL test
transport re-enters __memp_fget under rep latches); this reproduces
identically on plain master and is unrelated to this change (flagged to the
lead separately).

CBMC harness + LTS

The test/cbmc/harness_cmpvote.c / run.sh / README-H1 update lives on #263
(the branch that adds harness_cmpvote.c); it is prepared and verified but
depends on this fix being in the same tree (check_copies.sh), so the lead
sequences it when reconciling #263 with this fix. lts/2026.10.7 has the
identical function — backport PR: backport/2026.10.7/rep-elect-datagen.

agent added 3 commits November 4, 2026 10:57
Precondition reproduced against the real engine (dev box, non-diagnostic
--enable-test --enable-tcl): two/three electable sites with EQUAL last-commit
LSN (1/16402) and DIFFERENT data_gen (2 vs 3), real VOTE1 messages. Documents
the arm-D mechanism and the open reachability question (stale site elected only
if tallied last at a majority). Not yet a passing/failing gate; WIP probe.
…gen bypass)

The election winner comparator __rep_cmp_vote (src/rep/rep_elect.c) was not a
consistent order, with two defects:

  1. The first vote (rep->sites == 1) was adopted outright, so the winner
     depended on which vote a site tallied first.
  2. The last arm (equal LSN => higher priority / tiebreaker) fired regardless
     of data_gen, so a vote with an OLDER data_gen but an equal LSN and a
     higher priority or tiebreaker could beat a vote with a NEWER data_gen.

Defect 2 is a durability bug, not just a failure to settle: electing a site
with older data rolls back a commit the newer site already had (a client
memcmp's the log bytes at its verify LSN in __rep_verify, so a wrong winner
genuinely truncates the loser's tail).  The precondition -- two electable
sites with EQUAL last-commit LSN and DIFFERENT data_gen -- was reproduced
against the live engine (test/rep/E1-FINDINGS.md): a VOTE1 carries the LSN of
the last __txn_regop and the highest gen in REPLSNHIST, and these are
independent, so equal LSN with different data_gen is reachable.

Rewrite __rep_cmp_vote as a single TOTAL ORDER, applied uniformly to every
vote including the first: electable > not; non-zero priority > zero (the
mixed-version ELECTABLE semantics are preserved); then data_gen, LSN, priority,
tiebreaker, and finally eid as the always-distinct discriminator so the
relation is total even if two sites draw equal random tiebreakers.  Because
rep->winner is not reset between elections, the first vote is detected by
rep->sites == 1 (reset in __rep_elect_done), not by rep->winner.

Gate: test/db/run_rep_cmpvote_order.sh drives a verbatim copy of the real
comparator (drift-checked by check_rep_cmpvote_copy.sh) over the captured vote
vectors in every arrival order; it PASSES on the fix and FAILS with -DCMPVOTE_OLD
(the pre-fix comparator).  lts/2026.10.7 has the identical function; backport to
follow.
test/rep/E1-FINDINGS.md: the full write-up for the __rep_cmp_vote defect --
what is proven (comparator non-total-order; the equal-LSN/different-data_gen
precondition reproduced against the live engine), what is argued (a real
network delivering the stale VOTE1 last at a majority), the fix, and the
mixed-version rolling-upgrade analysis with a user-facing upgrade note.

test/tcl/rep200.tcl: an ADVISORY reproduction that builds the precondition
(three electable sites, equal last-commit LSN, data_gen 2/3/3) against the real
engine and reports it; its header explains why a deterministic end-to-end TCL
election gate is not possible here and points to the comparator-level gate
(test/db/run_rep_cmpvote_order.sh) and the CBMC harness as the pass/fail checks.
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Coccinelle convention checks

No new violations. ✅

Resolved since baseline (3) -- update dist/cocci/baseline.txt to lock these in.
rule_mutex_unbalanced|MUTEX_UNBALANCED|src/btree/bt_curadj.c|return (ret);
rule_mutex_unbalanced|MUTEX_UNBALANCED|src/crypto/mersenne/mt19937db.c|return (ret);
rule_mutex_unbalanced|MUTEX_UNBALANCED|src/mp/mp_register.c|return (ret);

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

ABI diff vs v2026.10.8 (libabigail — authoritative)

abidiff exit bitmask: 0

Removed exported symbols (nm -D, _NNNN version suffix normalized)

None.


Advisory: libabigail/nm is the authoritative binary-ABI check; Coccinelle is complementary source-level early warning. See dist/cocci/README.md.

…s not do

The comment called eid an always-distinct discriminator that makes every
site agree on the winner.  eids are local: repmgr numbers sites in the order
it learns of them, so two sites can give the same peer different eids.  The
eid step makes each site's order total, which is what order independence
needs.  Agreement between sites rests on the random 32-bit tiebreaker, as it
did before; an exact collision costs an extra election round, not a second
master.  Comment-only; the copy in rep_cmpvote_order.c follows.
@gburd

gburd commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Lead review: the fix is right and the runner fails on the old code and passes on the new (re-run locally). One overclaim corrected in c6e1030: the comment said the eid step makes every site agree on the winner. eids are local (repmgr numbers sites in the order it learns of them), so two sites can give the same peer different eids. The eid step makes each site's order total, which is what order independence needs. Agreement between sites still rests on the random 32-bit tiebreaker, as before; an exact collision costs an extra election round, not a second master. Comment-only. The diagnostic-build rep panic E1 found is a checker bug, fixed separately in #268.

@gburd
gburd marked this pull request as ready for review November 4, 2026 21:11
@gburd
gburd merged commit 2fd1c8d into master Nov 4, 2026
65 of 66 checks passed
@gburd
gburd deleted the fix/rep-elect-datagen branch November 4, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant