From 2c036042833d439b0e8c026b015c17f6a8529fc8 Mon Sep 17 00:00:00 2001
From: Paul Kim <mok03127@gmail.com>
Date: Sat, 12 Sep 2026 21:00:48 +0900
Subject: [PATCH v3 1/2] Honor the WAL insertion adjustment in
 XLogBackgroundFlush

WaitXLogInsertionsToFinish() adjusts a request that is past the end of
reserved WAL down to the current reserved position and returns it.
XLogBackgroundFlush() ignored that return value and passed its original
request to XLogWrite().

In a non-assert build, a bogus asyncXactLSN just after a segment
boundary can consequently advance the advertised write and flush
positions through the new page header.  A walsender can send that
header alone, after which a standby can interpret stale contents of a
recycled segment as a record.  Assert builds instead fail the
Insert >= Write invariant.

Use the returned position when it is smaller than the request, and
adjust both the write and flush targets.  Do not assign it
unconditionally, because the normal return value can be beyond the
requested position.

Also update the header comment of WaitXLogInsertionsToFinish(), which
claimed that the return value is always >= 'upto', contradicting the
adjustment documented in the function body.
---
 src/backend/access/transam/xlog.c | 28 +++++++++++++++++++++++++---
 1 file changed, 25 insertions(+), 3 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 0bd4ae12420..05185a5e411 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -1540,7 +1540,10 @@ WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt)
  * Returns the location of the oldest insertion that is still in-progress.
  * Any WAL prior to that point has been fully copied into WAL buffers, and
  * can be flushed out to disk. Because this waits for any insertions older
- * than 'upto' to finish, the return value is always >= 'upto'.
+ * than 'upto' to finish, the return value is normally >= 'upto'.  However,
+ * if 'upto' is past the end of reserved WAL, the request is adjusted down
+ * to the current reserved position, and the return value can be smaller
+ * than 'upto'.  Callers must not write or flush past the returned position.
  *
  * Note: When you are about to write out WAL, you must call this function
  * *before* acquiring WALWriteLock, to avoid deadlocks. This function might
@@ -3008,6 +3011,7 @@ bool
 XLogBackgroundFlush(void)
 {
 	XLogwrtRqst WriteRqst;
+	XLogRecPtr	insertpos;
 	bool		flexible = true;
 	static TimestampTz lastflush;
 	TimestampTz now;
@@ -3111,8 +3115,26 @@ XLogBackgroundFlush(void)
 
 	START_CRIT_SECTION();
 
-	/* now wait for any in-progress insertions to finish and get write lock */
-	WaitXLogInsertionsToFinish(WriteRqst.Write);
+	/* now wait for any in-progress insertions to finish */
+	insertpos = WaitXLogInsertionsToFinish(WriteRqst.Write);
+
+	/*
+	 * Honor the adjustment if the request was past the end of reserved
+	 * WAL.  Note that we must not assign the return value unconditionally
+	 * the way XLogFlush() does: it is normally beyond the requested
+	 * position, and the targets chosen above are deliberately conservative
+	 * (Write backed off to a page boundary, Flush governed by
+	 * wal_writer_flush_after, possibly a write-only cycle).  So only ever
+	 * lower them, keeping Flush <= Write.
+	 */
+	if (insertpos < WriteRqst.Write)
+	{
+		WriteRqst.Write = insertpos;
+		if (WriteRqst.Flush > insertpos)
+			WriteRqst.Flush = insertpos;
+	}
+
+	/* get write lock */
 	LWLockAcquire(WALWriteLock, LW_EXCLUSIVE);
 	RefreshXLogWriteResult(LogwrtResult);
 	if (WriteRqst.Write > LogwrtResult.Write ||
-- 
2.50.1 (Apple Git-155)

