From cc3c2147cc3892141c9e56c5ea09f028bfb9a20f Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy Date: Wed, 30 Sep 2026 03:15:23 +0000 Subject: [PATCH v2] Fix WAL segment file descriptor leak on WAL read errors. Previously, the WAL segment file that a WAL reader opens was closed only when the reader was freed. The descriptor is a plain kernel file descriptor, not a virtual file descriptor and not a transient file, so fd.c does not track it and no resource owner owns it. An error thrown while reading WAL therefore leaks it for the rest of the session. As a result, a few hundred failed calls in one session are enough to reach the descriptor limit, after which the backend cannot open any file at all, catalog files included. A leaked descriptor also pins a segment that has since been removed, so its space is not freed and the disk can fill up while pg_wal still looks small. The affected paths are pg_walinspect functions, logical decoding functions, the 2PC WAL read code, and the WAL summarizer. All of these except the WAL summarizer are reachable from SQL in simple ways. Fix this by registering a memory context reset callback on the context the reader is allocated in, which closes the segment file if that context is reset or deleted while the reader still holds it. XLogReaderFree() unregisters the callback before freeing the reader. Doing this in xlogreader.c covers every caller, present and future, instead of adding an error handler to each one. Note that PG18 and older cannot grow XLogReaderState, as it sits in a public header and its size must not change in a released branch, and they have no MemoryContextUnregisterResetCallback(). Backpatch to all supported versions. Author: Bharath Rupireddy Reviewed-by: Sami Imseih Reviewed-by: Michael Paquier Reviewed-by: Chao Li Discussion: https://postgr.es/m/CALj2ACVwDuOXXDjj2cVdnTKoxsgTSLDin4XoL63AnM6aUgMQaA@mail.gmail.com Backpatch-through: 14 --- src/backend/access/transam/xlogreader.c | 55 +++++++++++++++++++++++++ src/include/access/xlogreader.h | 12 ++++++ 2 files changed, 67 insertions(+) diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c index 7db7c273b0c..a1ca68905d9 100644 --- a/src/backend/access/transam/xlogreader.c +++ b/src/backend/access/transam/xlogreader.c @@ -36,6 +36,7 @@ #ifndef FRONTEND #include "pgstat.h" #include "storage/bufmgr.h" +#include "utils/memutils.h" #include "utils/wait_event.h" #else #include "common/logging.h" @@ -55,6 +56,9 @@ static bool ValidXLogRecord(XLogReaderState *state, XLogRecord *record, static void ResetDecoder(XLogReaderState *state); static void WALOpenSegmentInit(WALOpenSegment *seg, WALSegmentContext *segcxt, int segsize, const char *waldir); +#ifndef FRONTEND +static void xlogreader_close_segment(void *arg); +#endif /* size of the buffer allocated for error message. */ #define MAX_ERRORMSG_LEN 1000 @@ -159,9 +163,40 @@ XLogReaderAllocate(int wal_segment_size, const char *waldir, return state; } +#ifndef FRONTEND +/* + * Close the WAL segment file when the memory context holding the reader is + * reset or deleted, usually while an error is being handled. The reader is + * going away with that memory, so nothing can use the descriptor anymore. + * + * Reset callbacks run before the context's memory is freed, so the reader is + * still valid here. segment_close must not throw an error. + */ +static void +xlogreader_close_segment(void *arg) +{ + XLogReaderState *state = (XLogReaderState *) arg; + + if (state->seg.ws_file != -1) + state->routine.segment_close(state); +} +#endif + void XLogReaderFree(XLogReaderState *state) { +#ifndef FRONTEND + + /* + * Unregister the reset callback, which would otherwise be left pointing + * at freed memory. The context the reader was allocated in is the one it + * was registered on. + */ + if (state->reset_cb_registered) + MemoryContextUnregisterResetCallback(GetMemoryChunkContext(state), + &state->reset_cb); +#endif + if (state->seg.ws_file != -1) state->routine.segment_close(state); @@ -1597,6 +1632,26 @@ WALRead(XLogReaderState *state, state->routine.segment_close(state); XLByteToSeg(recptr, nextSegNo, state->segcxt.ws_segsize); + +#ifndef FRONTEND + + /* + * The WAL segment file is opened with BasicOpenFile(), so nothing + * but XLogReaderFree() ever closes it. An error thrown while + * reading WAL does not get that far, and the descriptor would + * then be leaked for the life of the process, so close it on a + * reset of the context the reader was allocated in as well. + */ + if (!state->reset_cb_registered) + { + state->reset_cb.func = xlogreader_close_segment; + state->reset_cb.arg = state; + MemoryContextRegisterResetCallback(GetMemoryChunkContext(state), + &state->reset_cb); + state->reset_cb_registered = true; + } +#endif + state->routine.segment_open(state, nextSegNo, &tli); /* This shouldn't happen -- indicates a bug in segment_open */ diff --git a/src/include/access/xlogreader.h b/src/include/access/xlogreader.h index 4a9a687e879..b109005dccd 100644 --- a/src/include/access/xlogreader.h +++ b/src/include/access/xlogreader.h @@ -315,6 +315,18 @@ struct XLogReaderState * data. */ bool nonblocking; + +#ifndef FRONTEND + + /* + * Reset callback on the memory context holding this reader, which closes + * the open WAL segment file if that context goes away before + * XLogReaderFree() is reached. Registered on the first segment_open() + * call, see WALRead(). + */ + MemoryContextCallback reset_cb; + bool reset_cb_registered; +#endif }; /* -- 2.47.3