From 50b3d1126e9fbc6ec62d648e33764c7cf8604be4 Mon Sep 17 00:00:00 2001
From: sai pranav <rajasaipranav0@gmail.com>
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 <rajasaipranav0@gmail.com>
Co-authored-by: Fujii Masao <masao.fujii@gmail.com>
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 &amp;&amp; cp pg_wal/0
     metadata to reconstruct the current version of the file.
    </para>
 
+   <para>
+    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.
+   </para>
+
    <para>
     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', <<EOM);
 INSERT INTO mytable VALUES (2, 'blackberry');
 EOM
 
+# Take another full backup on the old primary that requires WAL past the
+# switch point, making it unsuitable as a prior backup on node2.
+# Take it after promotion to prevent its checkpoint from advancing the
+# standby's WAL summarization beyond the recovery target.
+my $divergedpath = $node1->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

