Skip to content

Commit 8fcb32d

Browse files
committed
Add more protections in WAL record APIs against overflows
This commit adds a limit to the size of an XLogRecord at 1020MB, based on a suggestion by Heikki Linnakangas. This counts for the overhead needed by the XLogReader when allocating the memory it needs to read a record in DecodeXLogRecordRequiredSpace(), based on the record size. An assertion based on that is added to detect that any additions in the XLogReader facilities would not cause any overflows. If that's ever the case, the upper bound allowed would need to be adjusted. Before this, it was possible for an external module to create WAL records large enough to be assembled but not replayable, causing failures when replaying such WAL records on standbys. One case mentioned where this is possible is the in-core function pg_logical_emit_message() (wrapper for LogLogicalMessage), that allows to emit WAL records with an arbitrary amount of data potentially higher than the replay limit of approximately 1GB (limit of a palloc, minus the overhead needed by a XLogReader). This commit is a follow-up of ffd1b6b that has added similar protections for the block-level data. Here, the checks are extended to the whole record length, mainrdata_len being extended from uint32 to uint64 with the routines registering buffer and record data still limited to uint32 to minimize the checks when assembling a record. All the error messages related to overflow checks are improved to provide more context about the error happening. Author: Matthias van de Meent Reviewed-by: Andres Freund, Heikki Linnakangas, Michael Paquier Discussion: https://postgr.es/m/CAEze2WgGiw+LZt+vHf8tWqB_6VxeLsMeoAuod0N=ij1q17n5pw@mail.gmail.com
1 parent 26158b8 commit 8fcb32d

File tree

2 files changed

+65
-8
lines changed

2 files changed

+65
-8
lines changed

src/backend/access/transam/xloginsert.c

+54-8
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ static int max_registered_block_id = 0; /* highest block_id + 1 currently
9898
*/
9999
static XLogRecData *mainrdata_head;
100100
static XLogRecData *mainrdata_last = (XLogRecData *) &mainrdata_head;
101-
static uint32 mainrdata_len; /* total # of bytes in chain */
101+
static uint64 mainrdata_len; /* total # of bytes in chain */
102102

103103
/* flags for the in-progress insertion */
104104
static uint8 curinsert_flags = 0;
@@ -355,7 +355,10 @@ XLogRegisterData(char *data, uint32 len)
355355
Assert(begininsert_called);
356356

357357
if (num_rdatas >= max_rdatas)
358-
elog(ERROR, "too much WAL data");
358+
ereport(ERROR,
359+
(errmsg_internal("too much WAL data"),
360+
errdetail_internal("%u out of %u data segments are already in use.",
361+
num_rdatas, max_rdatas)));
359362
rdata = &rdatas[num_rdatas++];
360363

361364
rdata->data = data;
@@ -405,9 +408,16 @@ XLogRegisterBufData(uint8 block_id, char *data, uint32 len)
405408
* regbuf->rdata_len does not grow beyond what
406409
* XLogRecordBlockHeader->data_length can hold.
407410
*/
408-
if (num_rdatas >= max_rdatas ||
409-
regbuf->rdata_len + len > UINT16_MAX)
410-
elog(ERROR, "too much WAL data");
411+
if (num_rdatas >= max_rdatas)
412+
ereport(ERROR,
413+
(errmsg_internal("too much WAL data"),
414+
errdetail_internal("%u out of %u data segments are already in use.",
415+
num_rdatas, max_rdatas)));
416+
if (regbuf->rdata_len + len > UINT16_MAX || len > UINT16_MAX)
417+
ereport(ERROR,
418+
(errmsg_internal("too much WAL data"),
419+
errdetail_internal("Registering more than maximum %u bytes allowed to block %u: current %u bytes, adding %u bytes.",
420+
UINT16_MAX, block_id, regbuf->rdata_len, len)));
411421

412422
rdata = &rdatas[num_rdatas++];
413423

@@ -527,7 +537,7 @@ XLogRecordAssemble(RmgrId rmid, uint8 info,
527537
XLogRecPtr *fpw_lsn, int *num_fpi, bool *topxid_included)
528538
{
529539
XLogRecData *rdt;
530-
uint32 total_len = 0;
540+
uint64 total_len = 0;
531541
int block_id;
532542
pg_crc32c rdata_crc;
533543
registered_buffer *prev_regbuf = NULL;
@@ -841,8 +851,18 @@ XLogRecordAssemble(RmgrId rmid, uint8 info,
841851
{
842852
if (mainrdata_len > 255)
843853
{
854+
uint32 mainrdata_len_4b;
855+
856+
if (mainrdata_len > PG_UINT32_MAX)
857+
ereport(ERROR,
858+
(errmsg_internal("too much WAL data"),
859+
errdetail_internal("Main data length is %llu bytes for a maximum of %u bytes.",
860+
(unsigned long long) mainrdata_len,
861+
PG_UINT32_MAX)));
862+
863+
mainrdata_len_4b = (uint32) mainrdata_len;
844864
*(scratch++) = (char) XLR_BLOCK_ID_DATA_LONG;
845-
memcpy(scratch, &mainrdata_len, sizeof(uint32));
865+
memcpy(scratch, &mainrdata_len_4b, sizeof(uint32));
846866
scratch += sizeof(uint32);
847867
}
848868
else
@@ -872,13 +892,27 @@ XLogRecordAssemble(RmgrId rmid, uint8 info,
872892
for (rdt = hdr_rdt.next; rdt != NULL; rdt = rdt->next)
873893
COMP_CRC32C(rdata_crc, rdt->data, rdt->len);
874894

895+
/*
896+
* Ensure that the XLogRecord is not too large.
897+
*
898+
* XLogReader machinery is only able to handle records up to a certain
899+
* size (ignoring machine resource limitations), so make sure that we will
900+
* not emit records larger than the sizes advertised to be supported.
901+
* This cap is based on DecodeXLogRecordRequiredSpace().
902+
*/
903+
if (total_len >= XLogRecordMaxSize)
904+
ereport(ERROR,
905+
(errmsg_internal("oversized WAL record"),
906+
errdetail_internal("WAL record would be %llu bytes (of maximum %u bytes); rmid %u flags %u.",
907+
(unsigned long long) total_len, XLogRecordMaxSize, rmid, info)));
908+
875909
/*
876910
* Fill in the fields in the record header. Prev-link is filled in later,
877911
* once we know where in the WAL the record will be inserted. The CRC does
878912
* not include the record header yet.
879913
*/
880914
rechdr->xl_xid = GetCurrentTransactionIdIfAny();
881-
rechdr->xl_tot_len = total_len;
915+
rechdr->xl_tot_len = (uint32) total_len;
882916
rechdr->xl_info = info;
883917
rechdr->xl_rmid = rmid;
884918
rechdr->xl_prev = InvalidXLogRecPtr;
@@ -1297,6 +1331,18 @@ log_newpage_range(Relation rel, ForkNumber forknum,
12971331
void
12981332
InitXLogInsert(void)
12991333
{
1334+
#ifdef USE_ASSERT_CHECKING
1335+
1336+
/*
1337+
* Check that any records assembled can be decoded. This is capped based
1338+
* on what XLogReader would require at its maximum bound. This code path
1339+
* is called once per backend, more than enough for this check.
1340+
*/
1341+
size_t max_required = DecodeXLogRecordRequiredSpace(XLogRecordMaxSize);
1342+
1343+
Assert(AllocSizeIsValid(max_required));
1344+
#endif
1345+
13001346
/* Initialize the working areas */
13011347
if (xloginsert_cxt == NULL)
13021348
{

src/include/access/xlogrecord.h

+11
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ typedef struct XLogRecord
6262
#define XLR_INFO_MASK 0x0F
6363
#define XLR_RMGR_INFO_MASK 0xF0
6464

65+
/*
66+
* XLogReader needs to allocate all the data of a WAL record in a single
67+
* chunk. This means that a single XLogRecord cannot exceed MaxAllocSize
68+
* in length if we ignore any allocation overhead of the XLogReader.
69+
*
70+
* To accommodate some overhead, this value allows for 4M of allocation
71+
* overhead, that should be plenty enough for what
72+
* DecodeXLogRecordRequiredSpace() expects as extra.
73+
*/
74+
#define XLogRecordMaxSize (1020 * 1024 * 1024)
75+
6576
/*
6677
* If a WAL record modifies any relation files, in ways not covered by the
6778
* usual block references, this flag is set. This is not used for anything

0 commit comments

Comments
 (0)