--- id: "bug-778" status: "fixed" status_updated: "2026-09-24" status_note: "" component: "controller" severity: "data-loss" timestamp: "2026-06-19T13:40:00Z" last_seen: "2026-09-19T09:00:00Z" occurrences: 2 files: ["src/controller/persist.c:1336 (apply_wal_entry) + src/controller/ha.c ha_apply_single_entry (~1570-1588)"] tags: ["ha", "wal", "crc-chain", "last_crc", "apply_wal_entry", "persist_wal_write_replicated", "cold-restart", "data-loss", "bug-661", "standby-replication", "recovery", "root-caused"] related: ["bug-777", "bug-661"] fix_commits: ["64ab210"] legacy: true --- # bug-778: persist_recover RECOVER_TRUNCATED: 'CRC chain break ... prev_crc=00000000 expected=' at the… ## Description persist_recover RECOVER_TRUNCATED: 'CRC chain break ... prev_crc=00000000 expected=' at the first replicated job entry on cold restart; committed jobs unrecoverable (joblist=0). ROOT CAUSE of bug-777's cold-restart data loss. ## Environment _Not recorded (migrated from buglog.json)._ ## Reproduction _Not recorded (migrated from buglog.json)._ ## Root cause On the standby replicated-apply path, ha_apply_single_entry first calls persist_wal_write_replicated() which correctly sets p->last_crc to the standby's SELF-computed chain crc (the stream carries no crc; the on-disk CRC chain is node-local, prev_crc==last_crc). It THEN builds a synthetic wal_entry_hdr with hdr.crc=0 and calls apply_wal_entry(), whose line 1336 `p->last_crc = hdr->crc` OVERWRITES the correct chain link with 0. The next replicated write computes prev_crc=(seg_offset==0?WAL_CRC_NONE:last_crc)=0 and stamps prev_crc=0 on disk. So the on-disk CRC chain breaks at the 2nd replicated entry. Invisible while live (standby serves in-memory entities; never replays its own WAL), detonates on COLD RESTART where replay_wal validates the chain -> RECOVER_TRUNCATED -> recovers only the clean prefix -> committed jobs lost + the truncated verdict drives the promotion-fence availability wedge (the other half of bug-777). NOT an fd-position/offset race (pwrite+p->lock are correct); it is shared CRC-chain state (p->last_crc) written by two paths where the logical-apply step clobbers the physical-writer's link with 0. ## Fix FIXED 2026-06-19 (commit 64ab210). apply_wal_entry is now LOGICAL entity-restore only (signature (p,type,lsn,payload,payload_len); no header); it advances the current_lsn high-water but NEVER touches p->last_crc. The CRC chain has a single owner: persist_wal_write_replicated (live) + replay_wal/persist_recover (recovery). ha_apply_single_entry drops the fabricated crc=0 header. Verified: RED->GREEN test test_replicated_apply_chain_survives_cold_restart (real ha_apply_wal_catchup_entry path, cold-replay -> RECOVER_CLEAN); controller suite 0 failures; 3-node rolling-SIGKILL soak on real ext2/3 fs 5/5 (was wedge+loss at round 3); full cold restart recovers all 6 jobs (was 0), CLEAN verdicts, normal election. Test callers updated: test_repl_apply, test_ha_role_lifecycle, test_egress_filter. ## Verification _Not recorded (migrated from buglog.json)._ ## References - Related: bug-777, bug-661