From c24a51d0ad2e01681cdaa7781dc016c18a7c0184 Mon Sep 17 00:00:00 2001
From: Paul Kim <mok03127@gmail.com>
Date: Wed, 2 Sep 2026 14:40:25 +0900
Subject: [PATCH v1] Honor WAL insertion clamp in XLogBackgroundFlush

WaitXLogInsertionsToFinish() clamps a request that is past the end of
reserved WAL and returns the safe position.  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
clamp 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
clamp documented in the function body.
---
 src/backend/access/transam/xlog.c | 20 +++++++++++++++++---
 1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index de4c96e135f..a81b0522663 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -1541,7 +1541,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 clamped 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
@@ -3011,6 +3014,7 @@ bool
 XLogBackgroundFlush(void)
 {
 	XLogwrtRqst WriteRqst;
+	XLogRecPtr	insertpos;
 	bool		flexible = true;
 	static TimestampTz lastflush;
 	TimestampTz now;
@@ -3114,8 +3118,18 @@ 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 clamp if the request was past the end of reserved WAL */
+	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)

