#include #include #include #include #include "request.h" #include "xce/types.h" /* Tests for xce_hostctrl_req() validation paths. * We test only early-return paths (before xce_request() is called) * since a live endpoint is not available in unit tests. */ static void test_hostctrl_req_null_ep(void) { xce_hostctrl_t ctrl = { .action = HOST_OPEN, .hostname = "host1" }; int rc = xce_hostctrl_req(NULL, &ctrl); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); } /* ── HA heartbeat sender (xce_ha_heartbeat_req) ─────────────────────────── * The HA heartbeat is an OP_nop request carrying a HeartbeatMsg body. The * sender builds that request and hands it to xce_request(); since unit tests * have no live endpoint, these tests round-trip the same request shape the * sender builds through xce_encode()/xce_decode() and assert the three * committed-position fields survive the wire. A companion guard confirms the * generic xce_nop_req still produces a body-less nop. */ /* Build the heartbeat request exactly as xce_ha_heartbeat_req does and encode * it into a heap io_packet_t. xce_decode() takes ownership of the packet * (free_packet + FREEUP), so the caller must NOT free the returned pkt — it is * consumed by the subsequent xce_decode(). Mirrors the test_wal_wire pattern. */ static io_packet_t *encode_heartbeat(uint64_t term, uint64_t committed_lsn, uint64_t committed_config_tag) { heartbeat_msg_t hb = { .sender_priority = 0, .term = term, .committed_lsn = committed_lsn, .committed_config_tag = committed_config_tag, }; xce_request_t request = { .opcode = OPCode_nop, .body_which = Request_body_heartbeatmsg, .body = { .heartbeatmsg = &hb }, }; io_packet_t *pkt = (io_packet_t *)calloc(1, sizeof(*pkt)); TEST_ASSERT_NOT_NULL(pkt); /* xce_encode returns 1 on success (mirrors xce_request's encode path). */ TEST_ASSERT_EQUAL_INT(1, xce_encode(&request, pkt)); return pkt; } static void test_ha_heartbeat_req_null_ep(void) { /* Mirror the nop sender's NULL-endpoint guard. */ int rc = xce_ha_heartbeat_req(NULL, 1, 1, 1); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); } static void test_ha_heartbeat_req_roundtrip_fields(void) { /* Arrange/Act: encode a heartbeat carrying a committed position, decode. */ io_packet_t *pkt = encode_heartbeat(7, 9, 4); xce_request_t req; memset(&req, 0, sizeof(req)); TEST_ASSERT_EQUAL_INT(0, xce_decode(pkt, &req)); /* consumes/frees pkt */ /* Assert: OP_nop + heartbeatmsg arm with the three fields intact. */ TEST_ASSERT_EQUAL_INT(OPCode_nop, req.opcode); TEST_ASSERT_EQUAL_INT(Request_body_heartbeatmsg, req.body_which); TEST_ASSERT_NOT_NULL(req.body.heartbeatmsg); TEST_ASSERT_EQUAL_UINT64(7, req.body.heartbeatmsg->term); TEST_ASSERT_EQUAL_UINT64(9, req.body.heartbeatmsg->committed_lsn); TEST_ASSERT_EQUAL_UINT64(4, req.body.heartbeatmsg->committed_config_tag); xce_free_request(&req); printf("test_ha_heartbeat_req_roundtrip_fields: PASS\n"); } /* Guard: the generic nop sender still produces a body-less nop request. */ static void test_nop_req_remains_bodyless(void) { xce_request_t request = { .opcode = OPCode_nop, .body_which = Request_body_nomsg }; io_packet_t *pkt = (io_packet_t *)calloc(1, sizeof(*pkt)); TEST_ASSERT_NOT_NULL(pkt); TEST_ASSERT_EQUAL_INT(1, xce_encode(&request, pkt)); xce_request_t req; memset(&req, 0, sizeof(req)); TEST_ASSERT_EQUAL_INT(0, xce_decode(pkt, &req)); /* consumes/frees pkt */ TEST_ASSERT_EQUAL_INT(OPCode_nop, req.opcode); TEST_ASSERT_EQUAL_INT(Request_body_nomsg, req.body_which); xce_free_request(&req); printf("test_nop_req_remains_bodyless: PASS\n"); } static void test_hostctrl_req_null_ctrl(void) { /* non-NULL dummy ep is enough; the NULL ctrl check fires first */ int rc = xce_hostctrl_req((endpoint_t*)0x1, NULL); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); } static void test_hostctrl_req_null_hostname(void) { xce_hostctrl_t ctrl = { .action = HOST_OPEN, .hostname = NULL }; int rc = xce_hostctrl_req((endpoint_t*)0x1, &ctrl); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); } static void test_hostctrl_req_empty_hostname(void) { xce_hostctrl_t ctrl = { .action = HOST_OPEN, .hostname = "" }; int rc = xce_hostctrl_req((endpoint_t*)0x1, &ctrl); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); } static void test_hostctrl_req_noop(void) { xce_hostctrl_t ctrl = { .action = HOST_DO_NONE, .hostname = "host1" }; int rc = xce_hostctrl_req((endpoint_t*)0x1, &ctrl); TEST_ASSERT_EQUAL_INT(0, rc); } static void test_hostctrl_req_reject_tag(void) { xce_hostctrl_t ctrl = { .action = HOST_TAG, .hostname = "host1" }; int rc = xce_hostctrl_req((endpoint_t*)0x1, &ctrl); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); } static void test_hostctrl_req_reject_untag(void) { xce_hostctrl_t ctrl = { .action = HOST_UNTAG, .hostname = "host1" }; int rc = xce_hostctrl_req((endpoint_t*)0x1, &ctrl); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); } static void test_hostctrl_req_reject_negative_action(void) { xce_hostctrl_t ctrl = { .action = -1, .hostname = "host1" }; int rc = xce_hostctrl_req((endpoint_t*)0x1, &ctrl); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); } /* init_reply_by_req must fully zero the reply so the generated * encode_RequestReply never reads a stale `redirect_host` pointer (and the * other common trailer fields) off an uninitialized stack reply. Regression * guard for the heap-use-after-free in encode_RequestReply / strlen seen on a * NOP/state-report reply whose redirect_host was stack garbage. */ static void test_init_reply_by_req_zeroes_reply(void) { /* Poison the whole reply with non-zero bytes — mirrors an uninitialized * stack reply whose redirect_host happens to be a (dangling) non-NULL. */ xce_reply_t reply; xce_request_t req = { .seq = 42, .opcode = OPCode_nop }; memset(&reply, 0xFF, sizeof(reply)); init_reply_by_req(&reply, &req); /* seq/opcode carried from the request */ TEST_ASSERT_EQUAL_UINT64(42, reply.seq); TEST_ASSERT_EQUAL_INT(OPCode_nop, reply.opcode); /* Trailer/common fields the encoder always reads must be cleared so a * handler that only sets data_which/data cannot leak garbage onto the wire * (or dereference a dangling redirect_host). */ TEST_ASSERT_NULL(reply.redirect_host); TEST_ASSERT_EQUAL_UINT(0, reply.redirect_port); TEST_ASSERT_EQUAL_INT(0, reply.status); TEST_ASSERT_EQUAL_INT(0, reply.data_which); } /* ── job-array-single-wal-record task 5.3b: extract_data() wire dispatch for * Data_data_jobArray ───────────────────────────────────────────────────── * extract_data() is the shared inbound-push reassembly step every receiver * (scheduler, API server) passes through inside vtask_entry() (xcectx.c) * before its own handler ever runs. Its switch had no case for * Data_data_jobArray -- it fell into `default: rc = -EINVAL`, and * vtask_entry()'s `if (callnext)` treats ANY nonzero return (including a * negative errno) as "reassembly complete, proceed" -- so the real payload * was silently freed and the receiver was handed an empty, zeroed * dataset_t{rows=NULL, nrows=0}. This test drives the REAL wire encode/ * decode path (mirrors xcesched_push_jobarray()'s exact construction, * sched.c, and apiserver_push_jobarray()'s identical shape, apiserver.c) so * the payload extract_data() sees here is genuinely wire-decoded, not a * hand-built struct. */ static void test_extract_data_job_array_passes_through_intact(void) { job_submit_data_t submit; xce_job_submit_t job_array; xce_data_t data; xce_request_t request; xce_request_t decoded; io_packet_t *pkt; dataset_t dataset; int rc; xce_job_submit_t *got; /* Arrange: build the exact wire shape xcesched_push_jobarray()/ * apiserver_push_jobarray() send: OPCode_push / Request_body_pushmsg * carrying one Data.jobArray descriptor (base jobid, array_size, one * shared submit spec) -- never chunked, exactly one per commit. */ memset(&submit, 0, sizeof(submit)); submit.queue = "normal"; submit.user = "tester"; submit.mintasks = 1; submit.maxtasks = 1; submit.resreq = "type == local"; memset(&job_array, 0, sizeof(job_array)); job_array.jobid = 70000; job_array.array_size = 12; job_array.submit = &submit; memset(&data, 0, sizeof(data)); data.table = Data_data_jobArray; data.data.job_array = &job_array; memset(&request, 0, sizeof(request)); request.opcode = OPCode_push; request.body_which = Request_body_pushmsg; request.body.pushmsg = &data; pkt = (io_packet_t *)calloc(1, sizeof(*pkt)); TEST_ASSERT_NOT_NULL(pkt); TEST_ASSERT_EQUAL_INT(1, xce_encode(&request, pkt)); memset(&decoded, 0, sizeof(decoded)); TEST_ASSERT_EQUAL_INT(0, xce_decode(pkt, &decoded)); /* consumes/frees pkt */ TEST_ASSERT_EQUAL_INT(Request_body_pushmsg, decoded.body_which); TEST_ASSERT_NOT_NULL(decoded.body.pushmsg); TEST_ASSERT_EQUAL_INT(Data_data_jobArray, decoded.body.pushmsg->table); /* Act: this is exactly what vtask_entry() calls on every inbound push. */ memset(&dataset, 0, sizeof(dataset)); dataset.table = decoded.body.pushmsg->table; rc = extract_data(&dataset, decoded.body.pushmsg); /* Assert: nonzero/"reassembly complete" (matches vtask_entry's * `if (callnext)` truthy contract -- a single, unchunked descriptor never * waits for more) AND the genuine payload, not an empty dataset. Before * the fix, rc was -EINVAL (still truthy, so vtask_entry proceeded * anyway) but dataset.rows stayed NULL/nrows stayed 0 -- exactly the * silent-drop bug this test pins down. */ TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_NOT_NULL(dataset.rows); TEST_ASSERT_EQUAL_INT(1, dataset.nrows); got = (xce_job_submit_t *)dataset.rows; TEST_ASSERT_EQUAL_UINT64(70000, got->jobid); TEST_ASSERT_EQUAL_UINT64(12, got->array_size); TEST_ASSERT_NOT_NULL(got->submit); TEST_ASSERT_EQUAL_STRING("normal", got->submit->queue); TEST_ASSERT_EQUAL_STRING("tester", got->submit->user); /* Cleanup: extract_data() transfers ownership of the decoded * xce_job_submit_t out of decoded.body.pushmsg->data.job_array into * dataset.rows (nulling the source pointer) -- free the now-hollow * container and the transferred descriptor separately. */ xce_free_request(&decoded); free_JobSubmit_ptr((xce_job_submit_t **)&dataset.rows); printf("test_extract_data_job_array_passes_through_intact: PASS\n"); } void test_request(void) { printf("\n>> host control request validation tests\n\n"); RUN_TEST(test_hostctrl_req_null_ep); RUN_TEST(test_hostctrl_req_null_ctrl); RUN_TEST(test_hostctrl_req_null_hostname); RUN_TEST(test_hostctrl_req_empty_hostname); RUN_TEST(test_hostctrl_req_noop); RUN_TEST(test_hostctrl_req_reject_tag); RUN_TEST(test_hostctrl_req_reject_untag); RUN_TEST(test_hostctrl_req_reject_negative_action); RUN_TEST(test_init_reply_by_req_zeroes_reply); printf("\n>> HA heartbeat sender round-trip tests\n\n"); RUN_TEST(test_ha_heartbeat_req_null_ep); RUN_TEST(test_ha_heartbeat_req_roundtrip_fields); RUN_TEST(test_nop_req_remains_bodyless); printf("\n>> extract_data() Data_data_jobArray wire dispatch tests\n\n"); RUN_TEST(test_extract_data_job_array_passes_through_intact); }