From 9a3c45afd90f69b4d658e47c06c37671018210df Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy Date: Mon, 21 Sep 2026 06:50:47 +0000 Subject: [PATCH] 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 XLogReaderAllocate() 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(). There the callback stays registered and its bookkeeping lives in a list private to xlogreader.c. Backpatch to all supported versions. Author: Bharath Rupireddy Discussion: https://postgr.es/m/ Backpatch-through: 14 --- src/backend/access/transam/xlogreader.c | 60 +++++++++++++++++++++++++ src/include/access/xlogreader.h | 7 +++ 2 files changed, 67 insertions(+) diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c index 7db7c273b0c..9f1c2e5dae1 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_reset_callback(void *arg); +#endif /* size of the buffer allocated for error message. */ #define MAX_ERRORMSG_LEN 1000 @@ -156,12 +160,68 @@ XLogReaderAllocate(int wal_segment_size, const char *waldir, * enlarged if necessary. */ allocate_recordbuf(state, 0); + +#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 we are allocated + * in as well. + */ + state->reset_cb = palloc_extended(sizeof(MemoryContextCallback), + MCXT_ALLOC_NO_OOM | MCXT_ALLOC_ZERO); + if (!state->reset_cb) + { + pfree(state->errormsg_buf); + pfree(state->readRecordBuf); + pfree(state->readBuf); + pfree(state); + return NULL; + } + state->reset_cb->func = xlogreader_reset_callback; + state->reset_cb->arg = state; + MemoryContextRegisterResetCallback(CurrentMemoryContext, state->reset_cb); +#endif + 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_reset_callback(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. + */ + MemoryContextUnregisterResetCallback(GetMemoryChunkContext(state), + state->reset_cb); + pfree(state->reset_cb); +#endif + if (state->seg.ws_file != -1) state->routine.segment_close(state); diff --git a/src/include/access/xlogreader.h b/src/include/access/xlogreader.h index 4a9a687e879..1f5c87c3466 100644 --- a/src/include/access/xlogreader.h +++ b/src/include/access/xlogreader.h @@ -315,6 +315,13 @@ struct XLogReaderState * data. */ bool nonblocking; + + /* + * 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. Backend only, NULL in frontend code. + */ + struct MemoryContextCallback *reset_cb; }; /* -- 2.47.3