feat(ha-config-bootstrap): add peer count and cert pre-flight validation (task 2.3)

- Implement validate_peer_count() in startup_validation.c: -EINVAL for even N>1
- Implement validate_cert_files() with err_buf output: access() check, writes
  missing path into err_buf, returns -ENOENT
- Wire validate_peer_count() in main.c after peer list parse; fatal exit on fail
- Wire validate_cert_files() in main.c before SSL init; fatal exit on fail
- Fix ASAN leak: SSL_CTX_free in ha_fini() with NULL guard
- Fix double-free: null c->ha.ssl_ctx in cluster_fini() before ha_fini(),
  so iomgr_fini() retains sole SSL_CTX ownership (design per ha.h comment)
- All 96 tests pass; all 12 ha_startup tests GREEN
This commit is contained in:
Rongsong Shen 2026-05-28 04:29:31 +00:00
parent 3ab2e6e4bf
commit 488fd1bf49
4 changed files with 34 additions and 2 deletions

View file

@ -45,7 +45,7 @@ Dependency order: startup validation (D) unblocks vtaskmgr before election (A);
- _Requirements: 4.3, 4.4_
- _Boundary: Startup Validation (main.c, cluster.h, cluster.c)_
- [ ] 2.3 Add peer count validation and cert pre-flight checks
- [x] 2.3 Add peer count validation and cert pre-flight checks
- In `main.c` after argument parsing: if `n_peers % 2 == 0 && n_peers > 1` → `fprintf(stderr, ...)` + `exit(1)` with message explaining odd-N requirement
- Call `access(cert_path, R_OK)` and `access(key_path, R_OK)` in `main.c` before any SSL init; fatal exit with full path on failure
- Fix ASAN leak: add `SSL_CTX_free(ha->ssl_ctx)` in `ha_fini()`

View file

@ -1047,6 +1047,8 @@ void cluster_fini(cluster_t* c)
c->ha.standby_endpoints = NULL;
}
/* iomgr owns the SSL_CTX lifetime; null ha's copy so ha_fini doesn't double-free */
c->ha.ssl_ctx = NULL;
/* Stop HA watchdog thread before tearing down other subsystems — P1-5 */
ha_fini(&c->ha);

View file

@ -454,6 +454,12 @@ void ha_fini(ha_state_t *ha)
return;
ha_stop_watchdog(ha); /* join watchdog thread before destroying mutex */
pthread_mutex_destroy(&ha->lock);
/* Fix ASAN leak: free the SSL context if one was allocated */
if (ha->ssl_ctx) {
SSL_CTX_free((SSL_CTX *)ha->ssl_ctx);
ha->ssl_ctx = NULL;
}
}
/* ── Redirect helpers used by join.c ───────────────────────── */

View file

@ -11,8 +11,11 @@
extern int myapp_run(myconf_t*);
/* forward declaration — implemented in startup_validation.c */
/* forward declarations — implemented in startup_validation.c */
int validate_vcpus(int vcpus);
int validate_peer_count(int n_peers);
int validate_cert_files(const char *cert_path, const char *key_path,
char *err_buf, size_t err_len);
static char doc[] = "xce_controller";
static char args_doc[] = "";
@ -166,6 +169,17 @@ int main(int argc, char* argv[])
arguments.peers_str);
return 1;
}
/* Req 4.1: reject even-N peer counts (cannot form safe majority) */
if (validate_peer_count(myconf.peers_cfg.candidates_count) != 0) {
fprintf(stderr,
"xce_controller: --peers: even peer count (%d) is not "
"allowed; Raft quorum requires an odd number of nodes "
"(1, 3, 5, ...)\n",
myconf.peers_cfg.candidates_count);
return 1;
}
/* CA cert derived from workdir — same infrastructure as certfile/keyfile */
snprintf(myconf.peers_cfg.ca_cert_path,
sizeof(myconf.peers_cfg.ca_cert_path),
@ -185,6 +199,16 @@ int main(int argc, char* argv[])
INFO("No --peers flag; running as single-node primary");
}
/* Req 4.2: cert pre-flight — fail early with a clear path in the message */
{
char cert_err[XCE_MAXPATH_LEN + 64];
if (validate_cert_files(cert_file, key_file,
cert_err, sizeof(cert_err)) != 0) {
fprintf(stderr, "xce_controller: %s\n", cert_err);
return 1;
}
}
myapp_run(&myconf);
mylog_fini();