Election carrier (ha_elect_ctx_t) is never freed today -- each election/PreVote round deliberately leaks the previous one because per-peer vote endpoints hold a bare pointer into it and tear down asynchronously, so a naive free risks UAF. Requirements scope this to a memory-lifetime-only fix: free once no vote endpoint can reference it, never free while one might, uniformly across the real-election and PreVote paths. No election/failover semantic change.
6.4 KiB
Requirements Document
Introduction
The HA election carrier (ha_elect_ctx_t, allocated as mc in both ha_start_election_async and ha_start_prevote_async, stored in ha->elect_ctx) is never freed today. Each failover, real-election, or PreVote round allocates a fresh carrier and deliberately leaks the previous one, because per-peer vote endpoints (vote_ep_ctx_t.ctx) hold a bare pointer into the carrier and are torn down asynchronously by endpoint_fini on the iomgr thread — a time the round-starting thread cannot cheaply observe. A naive free at round-start would risk a use-after-free if a still-live endpoint callback later dereferences the superseded carrier.
This feature gives the election carrier a real, safe lifetime: it is freed once no vote endpoint can still reference it, and never freed while one might. It applies uniformly to both the real-election and PreVote carrier paths, since they share ha->elect_ctx and the identical hazard — fixing one without the other leaves the UAF risk in place for the unfixed path.
This is a memory-lifetime fix only. It must not change election, PreVote, or failover semantics, and must not weaken or alter any existing correctness guarantee (including the bug-339/bug-340 vote-freshness fixes).
Full discovery context (problem statement, code pointers, deferred-priority rationale, implementation options) is in
.kiro/specs/ha-elect-ctx-lifecycle/brief.mdand.kiro/specs/ha-elect-ctx-lifecycle/research.md.
Boundary Context
- In scope: the lifetime of the
ha_elect_ctx_tcarrier allocated byha_start_election_asyncandha_start_prevote_async; safe reclamation once everyvote_ep_ctx_tthat references it (viavctx->ctx) has been freed; coverage of every capture site (raft_cb_send_vote_request,raft_cb_send_pre_vote_request) and everyvctxfree site (includingendpoint_finiFREEUP and error/abort paths that never reach a live endpoint); the memory-order discipline between the reclamation mechanism and the existingatomic_store(&ha->elect_ctx, ..., release)publish / acquire reads. - Out of scope: the PreVote protocol itself (shipped and validated in
ha-prevote); any change to election, vote-granting, or failover semantics; theha_elect_ctx_tstruct's field contents or tally logic; the async endpoint-teardown ordering itself (endpoint_fini,vote_ep_teardown_async) beyond observing when it completes. - Adjacent expectations: relies on the existing per-endpoint teardown path (
endpoint_finifreeingvctx) continuing to run exactly as it does today — this feature observes that teardown, it does not change when or how an endpoint tears down. Must remain compatible with the existing "do NOT free a prior ctx" UAF-caution comments elsewhere inha.c(the rdhup path, delaymgr teardown) — this feature is what finally makes one of those comments obsolete for the carrier specifically, without touching the others' own hazards.
Requirements
Requirement 1: Bounded carrier lifetime
Objective: As a maintainer, I want the election carrier to be freed once it is no longer referenced, so that a long-running controller does not accumulate one leaked carrier per failover/election/PreVote round indefinitely.
Acceptance Criteria
- When every
vote_ep_ctx_tthat captured a reference to anha_elect_ctx_tcarrier has been freed, the Controller shall reclaim that carrier. - The Controller shall reclaim a superseded carrier without requiring a bounded wait, timeout, or external trigger — reclamation shall be driven by the actual release of the last referencing endpoint.
- Under an ASAN build with leak detection enabled, running many consecutive election/PreVote rounds shall report zero leaked
ha_elect_ctx_tallocations.
Requirement 2: No use-after-free on the endpoint-teardown path
Objective: As a maintainer, I want carrier reclamation to never race a live endpoint's use of that carrier, so that fixing the leak cannot introduce a use-after-free.
Acceptance Criteria
- The Controller shall never free an
ha_elect_ctx_tcarrier while anyvote_ep_ctx_tstill holds an unfreed reference to it (vctx->ctx). - When a vote endpoint's asynchronous teardown (
endpoint_finiFREEUP) reads or dereferences its captured carrier, that carrier shall still be valid at the time of that read, regardless of whether a newer election/PreVote round has since started. - The Controller shall apply the same safe-reclamation mechanism to every capture site of the carrier (
raft_cb_send_vote_requestfor real elections,raft_cb_send_pre_vote_requestfor PreVote), so no capture site is left with the old leak-forever behavior once this feature lands. - The Controller shall apply the same safe-reclamation mechanism on every
vctxfree path, including error and abort paths that free avote_ep_ctx_twithout the endpoint ever reaching a live connected state.
Requirement 3: Uniform coverage across real-election and PreVote
Objective: As a maintainer, I want the fix applied identically to both carrier-allocating paths, so that neither path is left with the original UAF-risk-driven leak.
Acceptance Criteria
- The Controller shall use the same carrier-lifetime mechanism for carriers allocated by
ha_start_election_async(real election) andha_start_prevote_async(PreVote). - When a PreVote round is superseded by a new round (e.g. a suppressed disruptive rejoin or a fresh liveness-triggered campaign), the superseded round's carrier shall become eligible for reclamation exactly as a superseded real-election carrier does.
Requirement 4: No regression to election, PreVote, or failover correctness
Objective: As an operator, I want this memory-lifetime change to leave voting and failover behavior completely unchanged, so that fixing a leak cannot introduce a liveness or safety regression.
Acceptance Criteria
- The Controller shall preserve all existing vote-tally, quorum, and freshness-gate behavior (including the bug-339/bug-340 vote-freshness fixes) unchanged by this feature.
- The Controller shall preserve all existing election, PreVote, and failover timing and outcome behavior unchanged by this feature — this feature shall not be observable from vote outcomes, election timing, or failover convergence.
- A full HA soak (sequential and chaotic
/xce-test-haregression) run after this feature lands shall show no new job loss, divergence, or liveness stall attributable to this change.