feat(election-iomgr-refactor): implement vote_reply_msg_handler

Replace stub with full iomgr-native vote reply decoder:
- Reads RequestVoteReply fields into local scalars BEFORE capn_free (UAF fix)
- Term fencing: atomic update ha->term if peer term > ours
- Error path: decode failure counts peer as denied vote
- Test seam: xce_vote_reply_hook for unit testing without real packets
- 4 new ASAN-clean unit tests: field decode, denied vote, term fencing, no-UAF

The pre-existing vote_request_thread UAF in the old pthread path remains
but is targeted for removal in task 5.3 (old code removal).
This commit is contained in:
Rongsong Shen 2026-05-30 08:45:43 +00:00
parent 18c280429e
commit bd58e5045a
2 changed files with 464 additions and 6 deletions

View file

@ -1119,18 +1119,232 @@ static void on_vote_connected(endpoint_t *ep)
}
/*
* vote_reply_msg_handler: stub — implemented in task 2.3.
* Declared here so on_vote_connected can reference it.
* Signature matches msg_handler_t = int (*)(void*) from netio.h.
* xce_vote_reply_hook: test-only seam for vote_reply_msg_handler.
*
* Forward-declared here (before vote_reply_msg_handler) so the handler body
* can reference it. Defined only under XCE_TESTING; guarded below.
*
* When non-NULL, vote_reply_msg_handler() calls this INSTEAD OF the real
* handle_request_vote_reply() so tests can capture decoded fields without
* a live iomgr. Receives the decoded scalar fields — no capnp pointers.
*
* Pattern mirrors xce_vote_request_hook used in task 2.2.
*/
static int vote_reply_msg_handler(void *ep)
#ifdef XCE_TESTING
int (*xce_vote_reply_hook)(ha_state_t *ha, ha_elect_ctx_t *ctx, int peer_idx,
uint64_t term, int vote_granted,
uint64_t peer_tag, uint64_t peer_lsn) = NULL;
#endif /* XCE_TESTING */
/*
* vote_reply_dispatch: internal helper — calls handle_request_vote_reply or
* the test hook (when XCE_TESTING and hook is non-NULL).
*/
static void vote_reply_dispatch(ha_state_t *ha, ha_elect_ctx_t *ctx, int idx,
uint64_t term, int vote_granted,
uint64_t peer_tag, uint64_t peer_lsn)
{
(void)ep;
/* Task 2.3 will implement this; stub returns 0 to avoid crash if called */
#ifdef XCE_TESTING
if (xce_vote_reply_hook != NULL) {
xce_vote_reply_hook(ha, ctx, idx, term, vote_granted, peer_tag, peer_lsn);
return;
}
#endif
handle_request_vote_reply(ha, ctx, idx, term, vote_granted, peer_tag, peer_lsn);
}
/*
* vote_reply_msg_handler: iomgr message handler for incoming RequestVoteReply.
*
* Called by iomgr (via INVOKE_MSG_HANDLER) when a full packet arrives on an
* outgoing vote endpoint after on_vote_connected fired.
*
* Steps:
* 1. Cast ep to endpoint_t*; get packet from ep->fsm_in.pkt (owned by us).
* 2. Raw capnp decode: init capn_ctx, read outer Request wrapper.
* 3. read_RequestReply() into rep_s (capnp pointer — still valid).
* 4. read_RequestVoteReply() into vr_raw (scalars) BEFORE capn_free.
* 5. capn_free() — capnp segment released; vr_raw still valid (scalars only).
* 6. free_packet() + free(pkt) — packet memory released.
* 7. Term fencing: if vr_raw.term > ha->term, update ha->term atomically.
* 8. Call vote_reply_dispatch() with vr_raw scalar fields.
*
* On any decode error: treat peer as denied (grant=0, tag=0, lsn=0).
*
* ASAN safety: all capnp fields read before capn_free() — no heap-use-after-free.
* Requirements: 3.1 (iomgr packet), 3.2 (no capnp UAF), 3.3 (extract fields),
* 3.4 (term fencing).
*/
static int vote_reply_msg_handler(void *ep_raw)
{
endpoint_t *ep = (endpoint_t *)ep_raw;
vote_ep_ctx_t *vctx = (vote_ep_ctx_t *)ep->data;
ha_state_t *ha = vctx->ha;
ha_elect_ctx_t *ctx = vctx->ctx;
int idx = vctx->peer_idx;
io_packet_t *pkt = ep->fsm_in.pkt; /* ownership transferred by INVOKE_MSG_HANDLER */
int ok = 0;
/* Decoded reply fields — populated on success path */
uint64_t vr_term = 0;
int vr_granted = 0;
uint64_t vr_tag = 0;
uint64_t vr_lsn = 0;
if (!pkt || !pkt->data || pkt->sz == 0)
goto done;
{
struct capn capn_ctx;
struct Request outer_rq;
Request_ptr outer_rp;
struct RequestReply rep_s;
struct RequestVoteReply vr_raw;
int capn_ok;
capn_ok = capn_init_mem(&capn_ctx, (const uint8_t *)pkt->data,
(size_t)pkt->sz, 0);
if (capn_ok != 0)
goto done;
outer_rp.p = capn_getp(capn_root(&capn_ctx), 0, 1);
read_Request(&outer_rq, outer_rp);
if (outer_rq.body_which != Request_body_replymsg ||
!outer_rq.body.replymsg.p.data) {
capn_free(&capn_ctx);
goto done;
}
read_RequestReply(&rep_s, outer_rq.body.replymsg);
if (rep_s.data_which != RequestReply_data_requestvotereplymsg) {
capn_free(&capn_ctx);
goto done;
}
/* CRITICAL: copy all scalars BEFORE capn_free to avoid UAF.
* struct RequestVoteReply fields: term, voteGranted, peerConfigTag,
* peerLastLogLsn — all scalars; safe to copy before freeing segment. */
memset(&vr_raw, 0, sizeof(vr_raw));
read_RequestVoteReply(&vr_raw, rep_s.data.requestvotereplymsg);
capn_free(&capn_ctx); /* capnp segment released; vr_raw.* still valid */
vr_term = (uint64_t)vr_raw.term;
vr_granted = vr_raw.voteGranted ? 1 : 0;
vr_tag = (uint64_t)vr_raw.peerConfigTag;
vr_lsn = (uint64_t)vr_raw.peerLastLogLsn;
ok = 1;
}
done:
/* Release packet — INVOKE_MSG_HANDLER will null ep->fsm_in.pkt after return */
if (pkt) { free_packet(pkt); free(pkt); }
if (ok) {
/* Term fencing: if peer's term exceeds ours, update atomically */
uint64_t my_term = atomic_load_explicit(&ha->term, memory_order_acquire);
if (vr_term > my_term)
atomic_store_explicit(&ha->term, vr_term, memory_order_release);
vote_reply_dispatch(ha, ctx, idx, vr_term, vr_granted, vr_tag, vr_lsn);
} else {
/* Decode error: treat peer as denied */
vote_reply_dispatch(ha, ctx, idx,
atomic_load_explicit(&ha->term, memory_order_acquire),
0, 0, 0);
}
return 0;
}
#ifdef XCE_TESTING
/*
* vote_reply_build_pkt_for_test: test helper that encodes a RequestVoteReply
* into a newly allocated io_packet_t that vote_reply_msg_handler can decode.
*
* Wire format mirrors the real peer reply:
* Request{opcode=reply, body.replymsg=RequestReply{
* data_which=requestvotereplymsg,
* data.requestvotereplymsg=RequestVoteReply{term, voteGranted, ...}
* }}
*
* Returns a freshly allocated io_packet_t* on success, NULL on failure.
* Handler takes ownership: it calls free_packet()+free() when done.
* Tests set ep->fsm_in.pkt to the returned pointer before calling the handler.
*/
io_packet_t *vote_reply_build_pkt_for_test(uint64_t term, int vote_granted,
uint64_t peer_tag, uint64_t peer_lsn)
{
struct capn cs;
struct capn_segment *seg;
struct RequestVoteReply vr_capn;
RequestVoteReply_ptr vr_ptr;
struct RequestReply rr_capn;
RequestReply_ptr rr_ptr;
struct Request rq_capn;
Request_ptr rq_ptr;
io_packet_t *pkt;
int rc;
capn_init_malloc(&cs);
seg = capn_root(&cs).seg;
/* RequestVoteReply — all scalar fields */
memset(&vr_capn, 0, sizeof(vr_capn));
vr_capn.term = term;
vr_capn.voteGranted = vote_granted ? 1 : 0;
vr_capn.peerConfigTag = peer_tag;
vr_capn.peerLastLogLsn = peer_lsn;
vr_ptr = new_RequestVoteReply(seg);
write_RequestVoteReply(&vr_capn, vr_ptr);
/* RequestReply envelope */
memset(&rr_capn, 0, sizeof(rr_capn));
rr_capn.seq = 1;
rr_capn.opcode = OPCode_requestVote;
rr_capn.status = 0;
rr_capn.data_which = RequestReply_data_requestvotereplymsg;
rr_capn.data.requestvotereplymsg = vr_ptr;
rr_ptr = new_RequestReply(seg);
write_RequestReply(&rr_capn, rr_ptr);
/* Outer Request{opcode=reply} */
memset(&rq_capn, 0, sizeof(rq_capn));
rq_capn.opcode = OPCode_reply;
rq_capn.seq = 1;
rq_capn.body_which = Request_body_replymsg;
rq_capn.body.replymsg = rr_ptr;
rq_ptr = new_Request(seg);
write_Request(&rq_capn, rq_ptr);
capn_setp(capn_root(&cs), 0, rq_ptr.p);
pkt = calloc(1, sizeof(*pkt));
if (!pkt) { capn_free(&cs); return NULL; }
pkt->sz = capn_size(&cs);
rc = alloc_packet(pkt);
if (rc < 0) { capn_free(&cs); free(pkt); return NULL; }
capn_write_mem(&cs, (uint8_t *)pkt->data, (size_t)pkt->sz, 0);
capn_free(&cs);
return pkt;
}
/*
* vote_reply_msg_handler_for_test: expose vote_reply_msg_handler under
* XCE_TESTING so tests can call it directly (it is otherwise static).
* Sets ep->fsm_in.pkt = pkt then calls vote_reply_msg_handler(ep).
* Caller owns ep->data (vote_ep_ctx_t); pkt ownership is transferred.
*/
int vote_reply_msg_handler_for_test(endpoint_t *ep, io_packet_t *pkt)
{
ep->fsm_in.pkt = pkt;
return vote_reply_msg_handler(ep);
}
/*
* vote_ep_ctx_build_for_test: test helper that allocates a vote_ep_ctx_t,
* fills it from the supplied parameters, and wires up ep->data and