- Mark the PreVote denial log-reason nit RESOLVED in ha-prevote tasks notes (47f7338).
- File ha-elect-ctx-lifecycle spec scaffold (spec.json + brief.md) capturing the
pre-existing, bounded, UAF-sensitive election-carrier leak and the refcount/
teardown-ordered-free approach across both election and PreVote paths. Deferred
by decision — not folded into ha-prevote to avoid a risky lifetime refactor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3 KiB
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
callocs a fresh carrier and the prior one is deliberately leaked:
/* 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=0is used in the test command.
Scope (when specced)
- Reference-count the carrier (increment per
vctxthat captures it, decrement on eachvctxfree, 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_ctxfield 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(setvctx->ctx), endpoint teardown viaendpoint_fini(freesvctx).src/controller/ha.h:ha_elect_ctx_t,vote_ep_ctx_t.