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

62 lines
3 KiB
Markdown

# 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`.