From c35d167d40146b9c10a018ebe36012ba3e27e720 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1s=20Senart?= <ts@perfloop.ai>
Date: Mon, 2 Nov 2026 23:05:54 +0000
Subject: [PATCH v1] Free oversized decoded WAL records in XLogReaderFree()

A decoded record that does not fit in the reader's decode buffer is
allocated separately (XLogReadRecordAlloc() with allow_oversized).
Such records are freed only by XLogReleasePreviousRecord() or
ResetDecoder().  XLogReaderFree() called neither, so any oversized
record still in the decode queue was leaked when the reader was
destroyed.  The last record returned by XLogReadRecord() stays in the
queue until the next read, so this happens whenever the last record
read before XLogReaderFree() is larger than the decode buffer.

The leak lasts for the life of the process in the checkpointer.
CheckPointTwoPhase() calls XlogReadTwoPhaseData() for each prepared
transaction whose PREPARE record is before the redo pointer, and that
function allocates and frees a reader in checkpointer_context, which
is reset only after an error.  With five prepared transactions, each
with an 80220-byte PREPARE record and followed by a CHECKPOINT, the
checkpointer's used memory grew by 82744 bytes per transaction.  With
this fix it stays flat.

Fix by calling ResetDecoder() in XLogReaderFree() before the decode
buffer is freed.  ResetDecoder() frees only records that were allocated
separately, so a caller-supplied decode buffer is not affected.

This problem exists since 3f1ce97346, in PostgreSQL 15.

Backpatch-through: 15
---
 src/backend/access/transam/xlogreader.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c
index e05a831b6e..f5051ba217 100644
--- a/src/backend/access/transam/xlogreader.c
+++ b/src/backend/access/transam/xlogreader.c
@@ -178,6 +178,9 @@ XLogReaderFree(XLogReaderState *state)
 	if (state->seg.ws_file >= 0)
 		state->routine.segment_close(state);
 
+	/* Free any queued decoded records. */
+	ResetDecoder(state);
+
 	if (state->decode_buffer && state->free_decode_buffer)
 		pfree(state->decode_buffer);
 
@@ -1728,8 +1731,7 @@ WALRead(XLogReaderState *state,
  */
 
 /*
- * Private function to reset the state, forgetting all decoded records, if we
- * are asked to move to a new read position.
+ * Forget all decoded records, freeing any oversized ones.
  */
 static void
 ResetDecoder(XLogReaderState *state)
