fix(raft-ha-core-safety): close 5.10a — WAL segment rotation + endpoint UAF blocking real snapshot catch-up

Chain of fixes needed to get --force-snapshot soak to a real live PASS
(raft-ha-core-safety task 5.10a / ha-entity-snapshots task 7.1):

1. WAL segment rotation was gated on a hardcoded 64MB compile-time
   constant; trim_wal_segments() never deletes the active (currently-open)
   segment. At soak job counts a segment never rotated, so nothing was
   ever physically trimmable and ha_evaluate_repl_action always chose
   STREAM_WAL over SEND_SNAPSHOT for an empty-rejoining node. Fixed by
   threading a wal_segment_bytes override through persist_config_t
   (mirrors the --max-wal-records pattern: config.h default, persist.h
   setter, persist.c 3 usage sites + minimum-floor enforcement, 6-point
   CLI wiring through main.c/myconf.h/myapp.c/cluster.c) plus a new
   --wal-segment-bytes CLI flag. run-ha-soak.sh's --force-snapshot mode
   now sets WAL_SEGMENT_BYTES=4096 to force real rotation at soak scale.

2. This unmasked a genuine heap-use-after-free in INVOKE_MSG_HANDLER
   (include/netio.h): the macro calls ep->msg_handler(ep) synchronously
   (not via the vtask dispatcher, so the bug-1298 vtask ref/unref pairing
   doesn't cover it) then writes ep->fsm_in.pkt = NULL. A stale vote-reply
   handler chain (raft_on_vote_reply -> raft_cb_become_follower ->
   endpoint_unref) could drop the endpoint's last ref and free it inside
   the handler call, so the trailing write hit freed memory. This is the
   first-ever live exercise of a node receiving a snapshot transfer while
   mid-election. Fixed by holding a ref across the synchronous call, same
   pattern task 5.8 established for the async path.

With both fixes: --force-snapshot soak now PASSES live (committed
divergence=0, job_loss=0, confirmed real "HA: snapshot" install on the
restarted node) — first genuine live proof of this scenario. 3-round
--chaotic regression clean. Full 217-test controller suite green
before/after every fix in the chain.

Builds on the delaymgr starvation fix (b9ad018) and walcompare
checkpoint-floor fix (f3c86a6) from earlier in this investigation.
This commit is contained in:
Rongsong Shen 2026-07-02 04:11:20 +00:00
parent b9ad01878c
commit 1439e2655e
11 changed files with 115 additions and 40 deletions

View file

@ -32,6 +32,7 @@ const char* argp_program_bug_address = BUG_EMAIL;
#define OPT_SYNC_TIMEOUT 261
#define OPT_WAL_SYNC 262
#define OPT_MAX_WAL_RECORDS 263
#define OPT_WAL_SEGMENT_BYTES 264
struct arguments {
char* myname;
@ -46,6 +47,7 @@ struct arguments {
uint32_t sync_timeout_ms;
uint32_t wal_sync_ms;
uint32_t max_wal_records;
uint32_t wal_segment_bytes;
};
static struct argp_option options[] = {
@ -71,6 +73,10 @@ static struct argp_option options[] = {
{ "max-wal-records", OPT_MAX_WAL_RECORDS, "N", 0,
"WAL records since last checkpoint before an async checkpoint is "
"scheduled (0 = default; persist enforces a minimum of 100)" },
{ "wal-segment-bytes", OPT_WAL_SEGMENT_BYTES, "N", 0,
"Physical WAL segment file size in bytes; a segment is only eligible "
"for trim/delete after it rotates out (0 = default 64 MB; persist "
"enforces a minimum of 4096)" },
{ 0 }
};
@ -134,6 +140,13 @@ static error_t parse_opt(int key, char* arg, struct argp_state* state)
arguments->max_wal_records = (uint32_t)v;
break;
}
case OPT_WAL_SEGMENT_BYTES: {
long v = atol(arg);
if (v < 0)
argp_error(state, "--wal-segment-bytes must be >= 0 (got: %s)", arg);
arguments->wal_segment_bytes = (uint32_t)v;
break;
}
default:
return ARGP_ERR_UNKNOWN;
}
@ -188,6 +201,7 @@ int main(int argc, char* argv[])
arguments.sync_timeout_ms = 0; /* 0 = not set; use persist config default */
arguments.wal_sync_ms = 0; /* 0 = not set; use persist config default */
arguments.max_wal_records = 0; /* 0 = not set; use persist config default */
arguments.wal_segment_bytes = 0; /* 0 = not set; use persist config default */
argp_parse(&argp, argc, argv, 0, 0, &arguments);
@ -239,6 +253,7 @@ int main(int argc, char* argv[])
myconf.sync_timeout_ms = arguments.sync_timeout_ms;
myconf.wal_sync_ms = arguments.wal_sync_ms;
myconf.max_wal_records = arguments.max_wal_records;
myconf.wal_segment_bytes = arguments.wal_segment_bytes;
memset(&myconf.peers_cfg, 0, sizeof(myconf.peers_cfg));
if (arguments.peers_str) {