From 6d3424c2fd0f20ddc73075bea22f9ad82d2b96d0 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 11 Aug 2026 11:41:28 +0000
Subject: [PATCH v1] Keep offline data checksum changes local
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Commit f19c0eccae96 made checkpoint records propagate the data checksum state.
This lets recovery restore online checksum transitions. However, pg_checksums
changes that state while the server is stopped and rewrites only the selected
data directory. Replaying a checkpoint from that node could enable verification
on a standby whose pages were never checksummed.

Mark checksum states written by pg_checksums as node-local. Also retain the end
LSN of the newest applied WAL-logged checksum transition. Before saving a primary’s
replayed checkpoint as a possible standby restartpoint, recovery replaces the
checkpoint’s checksum fields with the standby’s current local checksum state.

It ignores offline state from another node and transitions no newer than the
local watermark, while newer online transitions still take effect.

Preserve localized checkpoint fields across startup and backups taken from a
standby. Keep the current control-file state separate from the historical state
needed to restart replay.

Serialize checksum transition WAL insertion and publication with redo records.
Thus a transition cannot fall before the checkpoint redo point while leaving stale
state in that record.

Add TAP coverage for both cross node offline cases. Verify that an unchecked
standby neither adopts the primary offline transition nor becomes unreadable,
and that a standby local offline disable survives restart after an online
transition. Also exercise primary crash recovery plus standby clean and crash
restarts.

XXX: Bump control file format
XXX: Bump WAL format
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by:
Discussion:
Backpatch-through: 19
---
 src/backend/access/transam/xlog.c             | 211 +++++++++++++++---
 src/backend/access/transam/xlogrecovery.c     |   7 +-
 .../utils/activity/wait_event_names.txt       |   1 +
 src/bin/pg_checksums/pg_checksums.c           |   1 +
 src/bin/pg_resetwal/pg_resetwal.c             |   3 +
 src/include/access/xlog_internal.h            |   2 +
 src/include/access/xlogrecovery.h             |   3 +-
 src/include/catalog/pg_control.h              |   8 +-
 src/include/storage/lwlocklist.h              |   1 +
 src/test/modules/test_checksums/meson.build   |   1 +
 .../test_checksums/t/010_offline_standby.pl   |  87 ++++++++
 11 files changed, 285 insertions(+), 40 deletions(-)
  72.6% src/backend/access/transam/
   3.4% src/include/catalog/
  19.1% src/test/modules/test_checksums/t/
   4.7% src/

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index b23d8bbbdad..b8414a176a9 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -559,6 +559,10 @@ typedef struct XLogCtlData
 	/* last data_checksum_version we've seen */
 	uint32		data_checksum_version;
 
+	/* current checksum state and latest online transition */
+	XLogRecPtr	data_checksum_transition_lsn;
+	bool		data_checksum_state_is_local;
+
 	slock_t		info_lck;		/* locks shared variables shown above */
 } XLogCtlData;
 
@@ -676,6 +680,17 @@ static bool updateMinRecoveryPoint = true;
  */
 static ChecksumStateType LocalDataChecksumState = 0;
 
+/*
+ * This node's checksum state after replaying the latest XLOG_CHECKPOINT_REDO
+ * record. Preserve it until the matching XLOG_CHECKPOINT_ONLINE record is
+ * replayed, then store it in the restartpoint instead of the checksum state
+ * received from the primary.
+ */
+static ChecksumStateType replayedCheckpointDataChecksumState = 0;
+static XLogRecPtr replayedCheckpointDataChecksumTransitionLSN = InvalidXLogRecPtr;
+static bool replayedCheckpointDataChecksumStateIsLocal = false;
+static bool replayedCheckpointDataChecksumStateValid = false;
+
 /*
  * Variable backing the GUC, keep it in sync with LocalDataChecksumState.
  * See SetLocalDataChecksumState().
@@ -749,7 +764,7 @@ static void WALInsertLockAcquireExclusive(void);
 static void WALInsertLockRelease(void);
 static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
 
-static void XLogChecksums(uint32 new_type);
+static XLogRecPtr XLogChecksums(uint32 new_type);
 
 /*
  * Insert an XLOG record represented by an already-constructed chain of data
@@ -4283,12 +4298,16 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
 	ControlFile->wal_log_hints = wal_log_hints;
 	ControlFile->track_commit_timestamp = track_commit_timestamp;
 	ControlFile->data_checksum_version = data_checksum_version;
+	ControlFile->data_checksum_transition_lsn = InvalidXLogRecPtr;
+	ControlFile->data_checksum_state_is_local = false;
 
 	/*
 	 * Set the data_checksum_version value into XLogCtl, which is where all
 	 * processes get the current value from.
 	 */
 	XLogCtl->data_checksum_version = data_checksum_version;
+	XLogCtl->data_checksum_transition_lsn = InvalidXLogRecPtr;
+	XLogCtl->data_checksum_state_is_local = false;
 }
 
 static void
@@ -4747,6 +4766,7 @@ DataChecksumsNeedVerify(void)
 void
 SetDataChecksumsOnInProgress(void)
 {
+	XLogRecPtr	transition_lsn;
 	uint64		barrier;
 
 	/*
@@ -4756,14 +4776,12 @@ SetDataChecksumsOnInProgress(void)
 	START_CRIT_SECTION();
 	MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
-	XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
-
-	SpinLockAcquire(&XLogCtl->info_lck);
-	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
-	SpinLockRelease(&XLogCtl->info_lck);
+	transition_lsn = XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
 
 	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 	ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
+	ControlFile->data_checksum_transition_lsn = transition_lsn;
+	ControlFile->data_checksum_state_is_local = false;
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
@@ -4800,6 +4818,7 @@ SetDataChecksumsOnInProgress(void)
 void
 SetDataChecksumsOn(void)
 {
+	XLogRecPtr	transition_lsn;
 	uint64		barrier;
 
 	SpinLockAcquire(&XLogCtl->info_lck);
@@ -4824,11 +4843,7 @@ SetDataChecksumsOn(void)
 	START_CRIT_SECTION();
 	MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
-	XLogChecksums(PG_DATA_CHECKSUM_VERSION);
-
-	SpinLockAcquire(&XLogCtl->info_lck);
-	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
-	SpinLockRelease(&XLogCtl->info_lck);
+	transition_lsn = XLogChecksums(PG_DATA_CHECKSUM_VERSION);
 
 	/*
 	 * Update the controlfile before waiting since if we have an immediate
@@ -4836,6 +4851,8 @@ SetDataChecksumsOn(void)
 	 */
 	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 	ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+	ControlFile->data_checksum_transition_lsn = transition_lsn;
+	ControlFile->data_checksum_state_is_local = false;
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
@@ -4864,6 +4881,7 @@ SetDataChecksumsOn(void)
 void
 SetDataChecksumsOff(void)
 {
+	XLogRecPtr	transition_lsn;
 	uint64		barrier;
 
 	SpinLockAcquire(&XLogCtl->info_lck);
@@ -4890,14 +4908,12 @@ SetDataChecksumsOff(void)
 		START_CRIT_SECTION();
 		MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
-		XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
-
-		SpinLockAcquire(&XLogCtl->info_lck);
-		XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
-		SpinLockRelease(&XLogCtl->info_lck);
+		transition_lsn = XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
 
 		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 		ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
+		ControlFile->data_checksum_transition_lsn = transition_lsn;
+		ControlFile->data_checksum_state_is_local = false;
 		UpdateControlFile();
 		LWLockRelease(ControlFileLock);
 
@@ -4928,14 +4944,12 @@ SetDataChecksumsOff(void)
 	/* Ensure that we don't incur a checkpoint during disabling checksums */
 	MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
-	XLogChecksums(PG_DATA_CHECKSUM_OFF);
-
-	SpinLockAcquire(&XLogCtl->info_lck);
-	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
-	SpinLockRelease(&XLogCtl->info_lck);
+	transition_lsn = XLogChecksums(PG_DATA_CHECKSUM_OFF);
 
 	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 	ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
+	ControlFile->data_checksum_transition_lsn = transition_lsn;
+	ControlFile->data_checksum_state_is_local = false;
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
@@ -5426,6 +5440,8 @@ XLOGShmemInit(void *arg)
 
 	/* Use the checksum info from control file */
 	XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+	XLogCtl->data_checksum_transition_lsn = ControlFile->data_checksum_transition_lsn;
+	XLogCtl->data_checksum_state_is_local = ControlFile->data_checksum_state_is_local;
 	SetLocalDataChecksumState(XLogCtl->data_checksum_version);
 
 	SpinLockInit(&XLogCtl->Insert.insertpos_lck);
@@ -5512,6 +5528,8 @@ BootStrapXLOG(uint32 data_checksum_version)
 	checkPoint.time = (pg_time_t) time(NULL);
 	checkPoint.oldestActiveXid = InvalidTransactionId;
 	checkPoint.dataChecksumState = data_checksum_version;
+	checkPoint.dataChecksumTransitionLSN = InvalidXLogRecPtr;
+	checkPoint.dataChecksumStateIsLocal = false;
 
 	TransamVariables->nextXid = checkPoint.nextXid;
 	TransamVariables->nextOid = checkPoint.nextOid;
@@ -5850,6 +5868,10 @@ StartupXLOG(void)
 	bool		didCrash;
 	bool		haveTblspcMap;
 	bool		haveBackupLabel;
+	bool		backupFromStandby;
+	uint32		localDataChecksumState;
+	XLogRecPtr	localDataChecksumTransitionLSN;
+	bool		localDataChecksumStateIsLocal;
 	XLogRecPtr	EndOfLog;
 	TimeLineID	EndOfLogTLI;
 	TimeLineID	newTLI;
@@ -5987,10 +6009,46 @@ StartupXLOG(void)
 	 * starting checkpoint, and sets InRecovery and ArchiveRecoveryRequested.
 	 * It also applies the tablespace map file, if any.
 	 */
+	localDataChecksumState = ControlFile->checkPointCopy.dataChecksumState;
+	localDataChecksumTransitionLSN = ControlFile->checkPointCopy.dataChecksumTransitionLSN;
+	localDataChecksumStateIsLocal = ControlFile->checkPointCopy.dataChecksumStateIsLocal;
 	InitWalRecovery(ControlFile, &wasShutdown,
-					&haveBackupLabel, &haveTblspcMap);
+					&haveBackupLabel, &haveTblspcMap,
+					&backupFromStandby);
 	checkPoint = ControlFile->checkPointCopy;
 
+	/*
+	 * The checkpoint copy in pg_control has been localized by this node.
+	 * InitWalRecovery rereads the original WAL record, so restore the local
+	 * fields unless a primary backup selected a different checkpoint. A
+	 * standby backup contains pages matching its localized state.
+	 */
+	if (!haveBackupLabel || backupFromStandby)
+	{
+		checkPoint.dataChecksumState = localDataChecksumState;
+		checkPoint.dataChecksumTransitionLSN = localDataChecksumTransitionLSN;
+		checkPoint.dataChecksumStateIsLocal = localDataChecksumStateIsLocal;
+		ControlFile->checkPointCopy.dataChecksumState = localDataChecksumState;
+		ControlFile->checkPointCopy.dataChecksumTransitionLSN = localDataChecksumTransitionLSN;
+		ControlFile->checkPointCopy.dataChecksumStateIsLocal = localDataChecksumStateIsLocal;
+	}
+
+	/*
+	 * A local offline operation override the state in the selected
+	 * checkpoint. Otherwise, restore that checkpoint's state before replay so
+	 * later online checksum records are applied in the right order.
+	 */
+	if (!ControlFile->data_checksum_state_is_local)
+	{
+		XLogCtl->data_checksum_version = checkPoint.dataChecksumState;
+		XLogCtl->data_checksum_transition_lsn = checkPoint.dataChecksumTransitionLSN;
+		XLogCtl->data_checksum_state_is_local = checkPoint.dataChecksumStateIsLocal;
+		SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+		ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+		ControlFile->data_checksum_transition_lsn = checkPoint.dataChecksumTransitionLSN;
+		ControlFile->data_checksum_state_is_local = checkPoint.dataChecksumStateIsLocal;
+	}
+
 	/* initialize shared memory variables from the checkpoint record */
 	TransamVariables->nextXid = checkPoint.nextXid;
 	TransamVariables->nextOid = checkPoint.nextOid;
@@ -6603,10 +6661,9 @@ StartupXLOG(void)
 	 */
 	if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
 	{
-		XLogChecksums(PG_DATA_CHECKSUM_OFF);
+		(void) XLogChecksums(PG_DATA_CHECKSUM_OFF);
 
 		SpinLockAcquire(&XLogCtl->info_lck);
-		XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
 		SetLocalDataChecksumState(XLogCtl->data_checksum_version);
 		SpinLockRelease(&XLogCtl->info_lck);
 
@@ -6624,10 +6681,9 @@ StartupXLOG(void)
 	 */
 	else if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
 	{
-		XLogChecksums(PG_DATA_CHECKSUM_OFF);
+		(void) XLogChecksums(PG_DATA_CHECKSUM_OFF);
 
 		SpinLockAcquire(&XLogCtl->info_lck);
-		XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
 		SetLocalDataChecksumState(XLogCtl->data_checksum_version);
 		SpinLockRelease(&XLogCtl->info_lck);
 
@@ -6654,6 +6710,8 @@ StartupXLOG(void)
 
 	SpinLockAcquire(&XLogCtl->info_lck);
 	ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+	ControlFile->data_checksum_transition_lsn = XLogCtl->data_checksum_transition_lsn;
+	ControlFile->data_checksum_state_is_local = XLogCtl->data_checksum_state_is_local;
 	XLogCtl->SharedRecoveryState = RECOVERY_STATE_DONE;
 	SpinLockRelease(&XLogCtl->info_lck);
 
@@ -7524,6 +7582,8 @@ CreateCheckPoint(int flags)
 	 */
 	SpinLockAcquire(&XLogCtl->info_lck);
 	checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+	checkPoint.dataChecksumTransitionLSN = XLogCtl->data_checksum_transition_lsn;
+	checkPoint.dataChecksumStateIsLocal = XLogCtl->data_checksum_state_is_local;
 	SpinLockRelease(&XLogCtl->info_lck);
 
 	if (shutdown)
@@ -7580,10 +7640,13 @@ CreateCheckPoint(int flags)
 	{
 		xl_checkpoint_redo redo_rec;
 
+		LWLockAcquire(DataChecksumsStateLock, LW_EXCLUSIVE);
 		WALInsertLockAcquire();
 		redo_rec.wal_level = wal_level;
 		SpinLockAcquire(&XLogCtl->info_lck);
 		redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+		redo_rec.data_checksum_transition_lsn = XLogCtl->data_checksum_transition_lsn;
+		redo_rec.data_checksum_state_is_local = XLogCtl->data_checksum_state_is_local;
 		SpinLockRelease(&XLogCtl->info_lck);
 		WALInsertLockRelease();
 
@@ -7591,6 +7654,7 @@ CreateCheckPoint(int flags)
 		XLogBeginInsert();
 		XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo));
 		(void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO);
+		LWLockRelease(DataChecksumsStateLock);
 
 		/*
 		 * XLogInsertRecord will have updated XLogCtl->Insert.RedoRecPtr in
@@ -7941,6 +8005,8 @@ CreateEndOfRecoveryRecord(void)
 	/* start with the latest checksum version (as of the end of recovery) */
 	SpinLockAcquire(&XLogCtl->info_lck);
 	ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+	ControlFile->data_checksum_transition_lsn = XLogCtl->data_checksum_transition_lsn;
+	ControlFile->data_checksum_state_is_local = XLogCtl->data_checksum_state_is_local;
 	SpinLockRelease(&XLogCtl->info_lck);
 
 	UpdateControlFile();
@@ -8292,8 +8358,15 @@ CreateRestartPoint(int flags)
 				ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
 		}
 
-		/* we shall start with the latest checksum version */
-		ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
+		/*
+		 * Keep the current checksum state separate from the historical state
+		 * in checkPointCopy, which is used when replay starts again.
+		 */
+		SpinLockAcquire(&XLogCtl->info_lck);
+		ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+		ControlFile->data_checksum_transition_lsn = XLogCtl->data_checksum_transition_lsn;
+		ControlFile->data_checksum_state_is_local = XLogCtl->data_checksum_state_is_local;
+		SpinLockRelease(&XLogCtl->info_lck);
 
 		UpdateControlFile();
 	}
@@ -8734,9 +8807,9 @@ XLogReportParameters(void)
 }
 
 /*
- * Log the new state of checksums
+ * Log and publish the new state of checksums
  */
-static void
+static XLogRecPtr
 XLogChecksums(uint32 new_type)
 {
 	xl_checksum_state xlrec;
@@ -8744,11 +8817,23 @@ XLogChecksums(uint32 new_type)
 
 	xlrec.new_checksum_state = new_type;
 
+	LWLockAcquire(DataChecksumsStateLock, LW_EXCLUSIVE);
+
 	XLogBeginInsert();
 	XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
 
 	recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
 	XLogFlush(recptr);
+
+	SpinLockAcquire(&XLogCtl->info_lck);
+	XLogCtl->data_checksum_version = new_type;
+	XLogCtl->data_checksum_transition_lsn = recptr;
+	XLogCtl->data_checksum_state_is_local = false;
+	SpinLockRelease(&XLogCtl->info_lck);
+
+	LWLockRelease(DataChecksumsStateLock);
+
+	return recptr;
 }
 
 /*
@@ -8933,10 +9018,24 @@ xlog_redo(XLogReaderState *record)
 			ProcArrayApplyRecoveryInfo(&running);
 		}
 
+		/*
+		 * Checkpoint checksum state is local to the node that wrote it.
+		 * Preserve this node's state before retaining the checkpoint as a
+		 * possible restartpoint.
+		 */
+		SpinLockAcquire(&XLogCtl->info_lck);
+		checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
+		checkPoint.dataChecksumTransitionLSN = XLogCtl->data_checksum_transition_lsn;
+		checkPoint.dataChecksumStateIsLocal = XLogCtl->data_checksum_state_is_local;
+		SpinLockRelease(&XLogCtl->info_lck);
+		replayedCheckpointDataChecksumStateValid = false;
+
 		/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
 		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 		ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
 		ControlFile->data_checksum_version = checkPoint.dataChecksumState;
+		ControlFile->data_checksum_transition_lsn = checkPoint.dataChecksumTransitionLSN;
+		ControlFile->data_checksum_state_is_local = checkPoint.dataChecksumStateIsLocal;
 
 		UpdateControlFile();
 		LWLockRelease(ControlFileLock);
@@ -9000,6 +9099,18 @@ xlog_redo(XLogReaderState *record)
 								  checkPoint.oldestXid))
 			SetTransactionIdLimit(checkPoint.oldestXid,
 								  checkPoint.oldestXidDB);
+
+		/*
+		 * Use this node's state from the matching redo record. Using the
+		 * state at checkpoint completion would be wrong if an online
+		 * transition occurred while the checkpoint was running.
+		 */
+		Assert(replayedCheckpointDataChecksumStateValid);
+		checkPoint.dataChecksumState = replayedCheckpointDataChecksumState;
+		checkPoint.dataChecksumTransitionLSN = replayedCheckpointDataChecksumTransitionLSN;
+		checkPoint.dataChecksumStateIsLocal = replayedCheckpointDataChecksumStateIsLocal;
+		replayedCheckpointDataChecksumStateValid = false;
+
 		/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
 		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 		ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
@@ -9180,12 +9291,29 @@ xlog_redo(XLogReaderState *record)
 
 		memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
 
+		/*
+		 * An offline state overrites transitions already replayed on this
+		 * node. Apply a newer online transition from the redo record, but
+		 * never propagate an offline state from the node that wrote it.
+		 */
 		SpinLockAcquire(&XLogCtl->info_lck);
-		XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
-		SetLocalDataChecksumState(redo_rec.data_checksum_version);
-		if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
-			new_state = true;
+		if (!redo_rec.data_checksum_state_is_local &&
+			redo_rec.data_checksum_transition_lsn >
+			XLogCtl->data_checksum_transition_lsn)
+		{
+			if (XLogCtl->data_checksum_version != redo_rec.data_checksum_version)
+				new_state = true;
+			XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+			XLogCtl->data_checksum_transition_lsn = redo_rec.data_checksum_transition_lsn;
+			XLogCtl->data_checksum_state_is_local = false;
+			SetLocalDataChecksumState(redo_rec.data_checksum_version);
+		}
+
+		replayedCheckpointDataChecksumState = XLogCtl->data_checksum_version;
+		replayedCheckpointDataChecksumTransitionLSN = XLogCtl->data_checksum_transition_lsn;
+		replayedCheckpointDataChecksumStateIsLocal = XLogCtl->data_checksum_state_is_local;
 		SpinLockRelease(&XLogCtl->info_lck);
+		replayedCheckpointDataChecksumStateValid = true;
 
 		if (new_state)
 			EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version);
@@ -9249,15 +9377,28 @@ xlog2_redo(XLogReaderState *record)
 	if (info == XLOG2_CHECKSUMS)
 	{
 		xl_checksum_state state;
+		bool		apply;
 
 		memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
 
 		SpinLockAcquire(&XLogCtl->info_lck);
-		XLogCtl->data_checksum_version = state.new_checksum_state;
+		apply = record->EndRecPtr > XLogCtl->data_checksum_transition_lsn;
+
+		if (apply)
+		{
+			XLogCtl->data_checksum_version = state.new_checksum_state;
+			XLogCtl->data_checksum_transition_lsn = record->EndRecPtr;
+			XLogCtl->data_checksum_state_is_local = false;
+		}
 		SpinLockRelease(&XLogCtl->info_lck);
 
+		if (!apply)
+			return;
+
 		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 		ControlFile->data_checksum_version = state.new_checksum_state;
+		ControlFile->data_checksum_transition_lsn = record->EndRecPtr;
+		ControlFile->data_checksum_state_is_local = false;
 		UpdateControlFile();
 		LWLockRelease(ControlFileLock);
 
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 6de13b91748..96ff2749e1d 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -453,11 +453,13 @@ EnableStandbyMode(void)
  * PerformWalRecovery().
  *
  * This initializes some global variables like ArchiveRecoveryRequested, and
- * StandbyModeRequested and InRecovery.
+ * StandbyModeRequested and InRecovery. The output parameters describe the
+ * selected checkpoint and any backup metadata.
  */
 void
 InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr,
-				bool *haveBackupLabel_ptr, bool *haveTblspcMap_ptr)
+				bool *haveBackupLabel_ptr, bool *haveTblspcMap_ptr,
+				bool *backupFromStandby_ptr)
 {
 	XLogPageReadPrivate *private;
 	struct stat st;
@@ -982,6 +984,7 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr,
 	*wasShutdown_ptr = wasShutdown;
 	*haveBackupLabel_ptr = haveBackupLabel;
 	*haveTblspcMap_ptr = haveTblspcMap;
+	*backupFromStandby_ptr = backupFromStandby;
 }
 
 /*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 256b3a3c02e..c0a38640c46 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -371,6 +371,7 @@ WaitLSN	"Waiting to read or update shared Wait-for-LSN state."
 LogicalDecodingControl	"Waiting to read or update logical decoding status information."
 DataChecksumsWorker	"Waiting for data checksums worker."
 AioWorkerControl	"Waiting to update AIO worker information."
+DataChecksumsState	"Waiting to read or update data checksum state."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 3b3ae23f1a6..fa645d3af3a 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -647,6 +647,7 @@ main(int argc, char *argv[])
 	{
 		ControlFile->data_checksum_version =
 			(mode == PG_MODE_ENABLE) ? PG_DATA_CHECKSUM_VERSION : PG_DATA_CHECKSUM_OFF;
+		ControlFile->data_checksum_state_is_local = true;
 
 		if (do_sync)
 		{
diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 1542a56ca4b..fd012c10c46 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -914,6 +914,9 @@ RewriteControlFile(void)
 	XLogSegNoOffsetToRecPtr(newXlogSegNo, SizeOfXLogLongPHD, WalSegSz,
 							ControlFile.checkPointCopy.redo);
 	ControlFile.checkPointCopy.time = (pg_time_t) time(NULL);
+	ControlFile.checkPointCopy.dataChecksumState = ControlFile.data_checksum_version;
+	ControlFile.checkPointCopy.dataChecksumTransitionLSN = ControlFile.data_checksum_transition_lsn;
+	ControlFile.checkPointCopy.dataChecksumStateIsLocal = ControlFile.data_checksum_state_is_local;
 
 	ControlFile.state = DB_SHUTDOWNED;
 	ControlFile.checkPoint = ControlFile.checkPointCopy.redo;
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index be718993401..494748962f2 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -315,6 +315,8 @@ typedef struct xl_checkpoint_redo
 {
 	int			wal_level;
 	uint32		data_checksum_version;
+	XLogRecPtr	data_checksum_transition_lsn;
+	bool		data_checksum_state_is_local;
 } xl_checkpoint_redo;
 
 /*
diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h
index a1d8a81dbc1..46d7cedd518 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -155,7 +155,8 @@ extern PGDLLIMPORT bool StandbyMode;
 
 extern void InitWalRecovery(ControlFileData *ControlFile,
 							bool *wasShutdown_ptr, bool *haveBackupLabel_ptr,
-							bool *haveTblspcMap_ptr);
+							bool *haveTblspcMap_ptr,
+							bool *backupFromStandby_ptr);
 extern void PerformWalRecovery(void);
 
 /*
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 80b3a730e03..da5e2bb369d 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -64,8 +64,10 @@ typedef struct CheckPoint
 	 */
 	TransactionId oldestActiveXid;
 
-	/* data checksums state at the time of the checkpoint  */
+	/* data checksum state and latest online transition */
 	uint32		dataChecksumState;
+	XLogRecPtr	dataChecksumTransitionLSN;
+	bool		dataChecksumStateIsLocal;	/* set by offline pg_checksums */
 } CheckPoint;
 
 /* XLOG info values for XLOG rmgr */
@@ -228,8 +230,10 @@ typedef struct ControlFileData
 
 	bool		float8ByVal;	/* float8, int8, etc pass-by-value? */
 
-	/* Are data pages protected by checksums? Zero if no checksum version */
+	/* Current data checksum state and latest online transition */
 	uint32		data_checksum_version;
+	XLogRecPtr	data_checksum_transition_lsn;
+	bool		data_checksum_state_is_local;	/* set by offline pg_checksums */
 
 	/*
 	 * True if the default signedness of char is "signed" on a platform where
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index d7eb648bd27..0b18d6f4198 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -89,6 +89,7 @@ PG_LWLOCK(54, WaitLSN)
 PG_LWLOCK(55, LogicalDecodingControl)
 PG_LWLOCK(56, DataChecksumsWorker)
 PG_LWLOCK(57, AioWorkerControl)
+PG_LWLOCK(58, DataChecksumsState)
 
 /*
  * There also exist several built-in LWLock tranches.  As with the predefined
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
index 9b1421a9b91..fb84f678a3a 100644
--- a/src/test/modules/test_checksums/meson.build
+++ b/src/test/modules/test_checksums/meson.build
@@ -33,6 +33,7 @@ tests += {
       't/007_pgbench_standby.pl',
       't/008_pitr.pl',
       't/009_fpi.pl',
+      't/010_offline_standby.pl',
     ],
   },
 }
diff --git a/src/test/modules/test_checksums/t/010_offline_standby.pl b/src/test/modules/test_checksums/t/010_offline_standby.pl
new file mode 100644
index 00000000000..066541d84c4
--- /dev/null
+++ b/src/test/modules/test_checksums/t/010_offline_standby.pl
@@ -0,0 +1,87 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test interactions between offline checksum changes and streaming replication.
+# An offline change must remain local to its data directory: a standby must not
+# adopt the primary's offline change, and its own offline change must survive
+# WAL replay and restarts.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+my $primary = PostgreSQL::Test::Cluster->new('primary');
+$primary->init(allows_streaming => 1, no_data_checksums => 1);
+$primary->start;
+
+$primary->backup('backup');
+
+my $standby = PostgreSQL::Test::Cluster->new('standby');
+$standby->init_from_backup($primary, 'backup', has_streaming => 1);
+$standby->start;
+$primary->wait_for_replay_catchup($standby);
+
+test_checksum_state($primary, 'off');
+test_checksum_state($standby, 'off');
+
+# An offline change on the standby must override online transitions it has
+# already replayed.
+enable_data_checksums($primary, wait => 'on');
+wait_for_checksum_state($standby, 'on');
+$primary->wait_for_replay_catchup($standby);
+
+$standby->stop;
+$standby->checksum_disable_offline;
+$standby->start;
+test_checksum_state($standby, 'off');
+
+# Return both nodes to the initial state for the next test.
+disable_data_checksums($primary, wait => 1);
+$primary->wait_for_replay_catchup($standby);
+wait_for_checksum_state($standby, 'off');
+
+$standby->stop;
+$primary->stop;
+
+# This changes only the primary's pages and control file.
+$primary->checksum_enable_offline;
+$primary->start;
+test_checksum_state($primary, 'on');
+
+# Publish the primary's local state in a checkpoint, then make that checkpoint
+# the starting point for crash recovery on the same node.
+$primary->safe_psql('postgres', 'CHECKPOINT');
+$primary->stop('immediate');
+$primary->start;
+test_checksum_state($primary, 'on');
+
+# The standby must not adopt the primary's offline transition while replaying
+# the next checkpoint.
+$primary->safe_psql('postgres', 'CHECKPOINT');
+$standby->start;
+$primary->wait_for_replay_catchup($standby);
+test_checksum_state($standby, 'off');
+is($standby->safe_psql('postgres', 'SELECT count(*) FROM pg_class') > 0,
+	1, 'standby remains readable');
+
+# A clean stop creates a restartpoint. Its localized checksum state must
+# survive both a normal restart and subsequent crash recovery.
+$standby->stop;
+$standby->start;
+test_checksum_state($standby, 'off');
+
+$standby->stop('immediate');
+$standby->start;
+test_checksum_state($standby, 'off');
+
+$standby->stop;
+$primary->stop;
+
+done_testing();
-- 
2.34.1

