From e32c7550a594b0c15e68e62ad2afb4541338852d Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Fri, 9 Oct 2026 04:16:25 +0000
Subject: [PATCH v8 1/2] Fix end LSN reported for an xlog-switch record ending
 on a page boundary

A page header size is added to the end of an xlog-switch record even
when the record ends exactly on a page boundary and crosses no header.
At a segment boundary that puts the end past the end of reserved WAL.
A backend that requested the switch (pg_switch_wal(), or
do_pg_backup_start() and do_pg_backup_stop() during a backup) then
commits without an XID, so the position goes into the asynchronous
commit LSN, and from there into the WAL writer, which asks to write
and flush WAL that was never generated. If the page at the boundary
has not been initialized yet, XLogWrite() then PANICs with "xlog write
request ... is past end of log". Otherwise it reports write and flush
positions for WAL that was never written, and in an assert-enabled
build fails an assertion there.

Compute the end of the record with XLogBytePosToEndRecPtr(), the way
it is done for any other record, so that a record ending exactly on
a page boundary points to the boundary, and assert that it stays
within the reserved WAL. Also note in CheckArchiveTimeout() that a
switch record ending exactly on a segment boundary now looks like
no switch happened there, which only skips a debug message.

This also makes pg_switch_wal() return what its documentation says:
the ending location plus 1 within the just-completed WAL file.
---
 src/backend/access/transam/xlog.c     | 18 ++++++++----------
 src/backend/postmaster/checkpointer.c |  4 +++-
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 8636ad9ab2c..25ffc0a1e0c 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -1077,20 +1077,18 @@ XLogInsertRecord(XLogRecData *rdata,
 		/*
 		 * Even though we reserved the rest of the segment for us, which is
 		 * reflected in EndPos, we return a pointer to just the end of the
-		 * xlog-switch record.
+		 * xlog-switch record.  Compute it the same way as for any other
+		 * record, so that a record ending exactly on a page boundary points
+		 * to the boundary rather than past the next page header.  On a
+		 * segment boundary, the latter would be beyond the WAL we reserved.
 		 */
 		if (inserted)
 		{
-			EndPos = StartPos + SizeOfXLogRecord;
-			if (StartPos / XLOG_BLCKSZ != EndPos / XLOG_BLCKSZ)
-			{
-				uint64		offset = XLogSegmentOffset(EndPos, wal_segment_size);
+			XLogRecPtr	reservedEndPos PG_USED_FOR_ASSERTS_ONLY = EndPos;
 
-				if (offset == EndPos % XLOG_BLCKSZ)
-					EndPos += SizeOfXLogLongPHD;
-				else
-					EndPos += SizeOfXLogShortPHD;
-			}
+			EndPos = XLogBytePosToEndRecPtr(XLogRecPtrToBytePos(StartPos) +
+											MAXALIGN(SizeOfXLogRecord));
+			Assert(EndPos <= reservedEndPos);
 		}
 	}
 
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 02e4f6c338a..1761053ea2e 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -744,7 +744,9 @@ CheckArchiveTimeout(void)
 
 			/*
 			 * If the returned pointer points exactly to a segment boundary,
-			 * assume nothing happened.
+			 * assume nothing happened.  This is also what we get when a
+			 * switch record happens to end exactly on the boundary, but it
+			 * only costs us the debug message.
 			 */
 			if (XLogSegmentOffset(switchpoint, wal_segment_size) != 0)
 				elog(DEBUG1, "write-ahead log switch forced (\"archive_timeout\"=%d)",
-- 
2.50.1 (Apple Git-155)

