From 50b3d1126e9fbc6ec62d648e33764c7cf8604be4 Mon Sep 17 00:00:00 2001 From: sai pranav Date: Sat, 24 Oct 2026 01:50:50 +0530 Subject: [PATCH v2] Reject incremental backups based on a backup from a divergent history After a failover, an incremental backup could accept a prior backup whose WAL range extends beyond the point where the new primary left the old timeline. Combining these backups could retain pages from the abandoned history, producing a corrupt backup that might still recover and start successfully. The validation checked the end of the prior backup's final WAL range against the new backup's start LSN, but not against the timeline's switch point. This allowed an incompatible prior backup to pass validation once the new primary had advanced far enough. Reject incremental backups if the prior backup's final WAL range extends beyond the timeline's switch point. Backpatch to 17, where incremental backup was introduced. Author: Sai Pranav Co-authored-by: Fujii Masao Discussion: https://postgr.es/m/CAPHduZC9pWm_vjznmhZNFsDZKLimqc6dKXOozLUKZVPs19zbZg@mail.gmail.com Backpatch-through: 17 --- doc/src/sgml/backup.sgml | 7 +++++ src/backend/backup/basebackup_incremental.c | 13 +++++++++ src/bin/pg_combinebackup/t/008_promote.pl | 29 +++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml index 168444eccc5..4d3949c5388 100644 --- a/doc/src/sgml/backup.sgml +++ b/doc/src/sgml/backup.sgml @@ -877,6 +877,13 @@ test ! -f /mnt/server/archivedir/00000001000000A900000065 && cp pg_wal/0 metadata to reconstruct the current version of the file. + + The prior backup may be on an earlier timeline in the server's history, + but must not require WAL beyond the point where the server switched + away from that timeline. Otherwise, choose another prior backup or + take a new full backup. + + To figure out which blocks need to be backed up, the server uses WAL summaries, which are stored in the data directory, inside the directory diff --git a/src/backend/backup/basebackup_incremental.c b/src/backend/backup/basebackup_incremental.c index 8ba4f58c659..233da98043c 100644 --- a/src/backend/backup/basebackup_incremental.c +++ b/src/backend/backup/basebackup_incremental.c @@ -427,6 +427,19 @@ PrepareForIncrementalBackup(IncrementalBackupInfo *ib, if (range->tli == latest_wal_range_tli) { + /* + * If the prior backup's final timeline is now historic, its WAL + * range must not extend past the switch point. Otherwise, the + * backup may contain changes outside this server's history. + */ + if (XLogRecPtrIsValid(tlep[i]->end) && range->end_lsn > tlep[i]->end) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("manifest requires WAL from final timeline %u ending at %X/%08X, but this server switched timelines at %X/%08X", + range->tli, + LSN_FORMAT_ARGS(range->end_lsn), + LSN_FORMAT_ARGS(tlep[i]->end)))); + if (range->end_lsn > backup_state->startpoint) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), diff --git a/src/bin/pg_combinebackup/t/008_promote.pl b/src/bin/pg_combinebackup/t/008_promote.pl index f6d7a459676..321d2633f3a 100644 --- a/src/bin/pg_combinebackup/t/008_promote.pl +++ b/src/bin/pg_combinebackup/t/008_promote.pl @@ -2,6 +2,8 @@ # # Test whether WAL summaries are complete such that incremental backup # can be performed after promoting a standby at an arbitrary LSN. +# Also check that an incremental backup is rejected if the prior backup +# requires WAL past the timeline switch point. use strict; use warnings FATAL => 'all'; @@ -70,6 +72,20 @@ $node2->safe_psql('postgres', <backup_dir . '/diverged'; +$node1->command_ok( + [ + 'pg_basebackup', + '--pgdata' => $divergedpath, + '--no-sync', + '--checkpoint' => 'fast', + ], + "full backup on the old primary after the timeline switch"); + # Now take an incremental backup. If WAL summarization didn't follow the # timeline change correctly, something should break at this point. my $backup2path = $node1->backup_dir . '/backup2'; @@ -83,6 +99,19 @@ $node2->command_ok( ], "incremental backup from node2"); +# An incremental backup using the old primary's post-switch backup must fail. +$node2->command_fails_like( + [ + 'pg_basebackup', + '--pgdata' => $node1->backup_dir . '/incr_bad', + '--no-sync', + '--checkpoint' => 'fast', + '--incremental' => $divergedpath . '/backup_manifest', + ], + qr/manifest requires WAL from final timeline 1 ending at .*, but this server switched timelines at/, + "reject incremental backup using a prior backup from beyond the switch point" +); + # Restore the incremental backup and use it to create a new node. my $node3 = PostgreSQL::Test::Cluster->new('node3'); $node3->init_from_backup($node1, 'backup2', -- 2.56.0