myxce/.kiro/specs/ha-elect-ctx-lifecycle/brief.md
Rongsong Shen 5503c7a176 chore(ha-prevote): resolve log-reason nit; track elect_ctx leak as follow-up spec
- 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>
2026-06-16 07:56:07 +00:00

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=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.