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

@ -385,8 +385,14 @@ struct snapshot_transfer_s {
struct wal_catchup_entry {
uint64_t lsn;
uint8_t entry_type;
xce_data_t *payload; /* legacy decoded field — broken for
* raw bytes; kept NULL, do not use */
/* bug-1505 fix: List(UInt8) now, not Data (see controller.capnp's
* WalCatchupEntry.payload comment for the full root cause) — a real
* uint8_t* + wire_payload_len pair via the generic codec, same shape
* bytes_read/content already establish elsewhere. Still legacy/unused
* by production (wal_entry_to_wire() in request.c deliberately never
* sets it — see that comment); this genuinely round-trips now instead
* of being a documented-broken placeholder, but nothing reads it. */
uint8_t *payload;
int n_entries; /* batch: number of sub-entries */
struct wal_catchup_entry **entries; /* batch: array of sub-entry pointers */
/* Raw WAL entry payload bytes — same pattern as repl_config_push_req_t.
@ -421,15 +427,23 @@ struct wal_catchup_entry {
* send; used on decode to slice this entry's bytes out of the TOP-LEVEL
* container's batch_payloads blob (below). */
uint32_t wire_payload_len;
/* bug-847: generic decoded placeholder for wire field batchPayloads @10 —
* same broken-for-raw-bytes limitation as `payload` above; kept NULL, do
* not use. Only meaningful on the TOP-LEVEL container (never on a nested
* sub-entry). The real bytes are hand-extracted by xce_decode() into
* raw_batch_payloads/raw_batch_payloads_len below, then sliced per
/* bug-1505 fix: List(UInt8) now, not Data (controller.capnp's
* WalCatchupEntry.batchPayloads comment has the full root cause —
* same class as `payload` above and SnapshotData.internalState,
* bug-1504). Generic decoded field for wire field batchPayloads @10;
* genuinely round-trips now via the generic codec, but still unused by
* production — the real bytes are hand-extracted by xce_decode() into
* raw_batch_payloads/raw_batch_payloads_len below (a separate,
* already-correct code path this fix does not touch), then sliced per
* sub-entry using each entry's wire_payload_len into that entry's OWN
* raw_payload/raw_payload_len (so every consumer keeps reading
* raw_payload exactly as before — this is purely a wire-transport fix). */
xce_data_t *batch_payloads;
* raw_payload exactly as before). Only meaningful on the TOP-LEVEL
* container (never on a nested sub-entry). batch_payloads_wire_len is
* its own maplistcount companion (wire field batchPayloadsLen @11) —
* distinct from raw_batch_payloads_len below, which belongs to the
* separate hand-rolled extraction path. */
uint8_t *batch_payloads;
uint32_t batch_payloads_wire_len;
uint8_t *raw_batch_payloads;
size_t raw_batch_payloads_len;
};