# Discovery Brief: ha-elect-ctx-lifecycle _Filed 2026-06-16 as a tracked follow-up from the ha-prevote feature. Not yet specced — run `/kiro-spec-requirements ha-elect-ctx-lifecycle` to begin._ ## Problem 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**. Each failover/election/PreVote round `calloc`s a fresh carrier and the prior one is deliberately leaked: ```c /* ha.c ~3746 */ /* Do NOT free a prior ctx (live vote endpoints may hold a bare * pointer to it — same UAF caution as the rdhup path). */ atomic_store_explicit(&ha->elect_ctx, mc, memory_order_release); ``` Root cause: per-peer vote endpoints hold a bare pointer to the carrier (`vctx->ctx = ectx`). Those `vctx` are freed asynchronously by `endpoint_fini` (FREEUP) on the iomgr thread when the vote endpoint closes — at a time the round-starting thread cannot cheaply observe. There is **no actual `ha_election_cleanup()`** (only an aspirational comment at ha.c:3504). So freeing the carrier when a new round starts would risk a **use-after-free** if an endpoint callback still references it. ## Why it's not urgent - **Pre-existing**: the real-election path (`ha_start_election_async`) has the identical leak; ha-prevote reused the same carrier pattern and did **not** worsen the per-failover rate (a suppressed disruptive rejoin now leaks *one* PreVote carrier where it previously caused an election storm leaking several). - **Bounded**: one small struct (`sizeof(ha_elect_ctx_t) + n_peers*votes[]`, hundreds of bytes for a 3-node cluster) per failover/PreVote event. Failover is a rare event, not steady-state — no unbounded growth under normal operation. - Surfaced as ~174KB / ~1548 allocs under ASAN during the full controller test suite (many simulated rounds); `detect_leaks=0` is used in the test command. ## Scope (when specced) - Reference-count the carrier (increment per `vctx` that captures it, decrement on each `vctx` free, free at zero) **or** a teardown-ordered deferred free tied to confirmed vote-endpoint release. - Apply to **both** the real-election and PreVote carrier paths — they share the `ha->elect_ctx` field and the same UAF hazard; fixing one without the other leaves the hazard. - Must not regress failover correctness (bug-339/bug-340) or introduce a UAF on the async endpoint-teardown path. Validate under ASAN with leak detection **enabled** (the success criterion is zero carrier leaks across many rounds). ## Out of scope - The PreVote protocol itself (shipped and validated in `ha-prevote`). - Any change to election/voting *semantics* — this is purely memory lifetime. ## Pointers - `src/controller/ha.c`: `ha_start_election_async`, `ha_start_prevote_async` (carrier alloc + the "do NOT free prior ctx" comments ~3503, ~3746), `raft_cb_send_vote_request` / `ha_send_pre_vote_request` (set `vctx->ctx`), endpoint teardown via `endpoint_fini` (frees `vctx`). - `src/controller/ha.h`: `ha_elect_ctx_t`, `vote_ep_ctx_t`.