fix(controller): stop SIGSEGV in WalCatchupEntry capnp encode (bug-1505)

controller_unit_test crashed deterministically in write_WalCatchupEntry
(test_wal_catchup_append_term_fields_round_trip) -- the same bug class
as bug-1504 (SnapshotData.internalState): this codebase's capnpc-c
backend silently no-ops Data-field encoding in generated encode_XXX()
composite helpers without zero-initializing the caller's local struct
first, leaving an uninitialized capn_ptr written onto the wire.

Confirmed zero production exposure before fixing: xce_wal_batch_encode_
raw()/wal_entry_to_wire() (request.c) never call the generic
encode_WalCatchupEntry() at all -- they memset() their own struct and
call write_WalCatchupEntry() directly with every field hand-set, so
this was 100% test-only exposure (grepped every caller of the generic
encode/decode pair; only the crashing test uses it).

Fix mirrors bug-1504: WalCatchupEntry.payload (@2) and batchPayloads
(@10) switched from Data to List(UInt8) in controller.capnp (new
batchPayloadsLen @11 companion; payload reuses the existing
wirePayloadLen @9), typemap.h's wal_catchup_entry_t field types
updated, and request.c's extract_data_bytes() widened to take a raw
capn_ptr (it already only used .p internally) so its call sites work
regardless of which Data-shaped wrapper the generated accessor
returns now. batchPayloads' single-top-level-blob shape confirmed
this does not reintroduce bug-847's per-element cross-segment
far-pointer loss -- that issue is inherent to per-element data
regardless of Data vs List(UInt8) spelling.

Strengthened test_wal_catchup_append_term_fields_round_trip with real
payload/batchPayloads content assertions -- previously never
exercised, since the test's src fields were left NULL.

Verified: controller suite 3/3 (was 2/3, crashing), scheduler suite
unaffected at 37/37 (unrelated schema area).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPPXeKSUXUqcqvzNXY1rzJ
This commit is contained in:
Rongsong Shen 2026-08-07 11:09:27 +00:00
parent 829fbe41dc
commit 1fd467d072
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
7 changed files with 121 additions and 22 deletions

View file

@ -195,11 +195,21 @@ static void test_request_vote_last_log_tuple_round_trip(void)
/* ------------------------------------------------------------------ */
/* Test 5: WalCatchupEntry (replication envelope) round-trips the */
/* AppendEntries term fields (prev_log_index, prev_log_term, term) */
/* (ha-term-safety task 1.1, Req 1.3/1.4). */
/* (ha-term-safety task 1.1, Req 1.3/1.4), AND (bug-1505 fix) genuinely */
/* round-trips payload/batchPayloads content through the GENERIC codec */
/* now that both are List(UInt8) instead of Data -- this is the exact */
/* path that used to SIGSEGV (write_WalCatchupEntry writing an */
/* uninitialized capn_ptr for a no-op'd Data field) even though this */
/* test never previously touched payload content at all (src.payload */
/* stayed NULL); see controller.capnp's WalCatchupEntry.payload comment */
/* for the full root cause. */
/* ------------------------------------------------------------------ */
static void test_wal_catchup_append_term_fields_round_trip(void)
{
static const uint8_t entry_payload[] = { 0x11, 0x22, 0x33, 0x44, 0x55 };
static const uint8_t batch_payload[] = { 0xAA, 0xBB, 0xCC, 0xDD };
wal_catchup_entry_t src;
memset(&src, 0, sizeof(src));
src.lsn = 42ULL;
@ -207,6 +217,12 @@ static void test_wal_catchup_append_term_fields_round_trip(void)
src.term = 7ULL; /* NEW: this entry's term stamp */
src.prev_log_index = 41ULL; /* NEW: lsn - 1 */
src.prev_log_term = 6ULL; /* NEW: term at prev_log_index */
/* bug-1505 fix: real content through the generic codec (List(UInt8),
* not Data) for both payload and batchPayloads. */
src.payload = (uint8_t *)entry_payload;
src.wire_payload_len = (uint32_t)sizeof(entry_payload);
src.batch_payloads = (uint8_t *)batch_payload;
src.batch_payloads_wire_len = (uint32_t)sizeof(batch_payload);
struct capn c;
capn_init_malloc(&c);
@ -238,6 +254,20 @@ static void test_wal_catchup_append_term_fields_round_trip(void)
TEST_ASSERT_EQUAL_UINT64(41ULL, dst.prev_log_index); /* survives the wire */
TEST_ASSERT_EQUAL_UINT64(6ULL, dst.prev_log_term); /* survives the wire */
/* bug-1505 fix: payload/batchPayloads now genuinely round-trip through
* the generic codec (List(UInt8), not Data) -- this is the content the
* old no-op'd Data-field codec never actually carried. */
TEST_ASSERT_EQUAL_UINT32((uint32_t)sizeof(entry_payload),
dst.wire_payload_len);
TEST_ASSERT_NOT_NULL(dst.payload);
TEST_ASSERT_EQUAL_MEMORY(entry_payload, dst.payload, sizeof(entry_payload));
TEST_ASSERT_EQUAL_UINT32((uint32_t)sizeof(batch_payload),
dst.batch_payloads_wire_len);
TEST_ASSERT_NOT_NULL(dst.batch_payloads);
TEST_ASSERT_EQUAL_MEMORY(batch_payload, dst.batch_payloads,
sizeof(batch_payload));
capn_free(&d);
free(buf);
printf("test_wal_catchup_append_term_fields_round_trip: PASS\n");