Subject: Re: Fix uninitialized xl_running_xacts padding

From: Andres Freund <[email-masked]>
Date: Mon, 16 Feb 2026 15:10:40 -0500

Hi,

On 2026-02-16 12:02:33 +0100, Anthonin Bonnefoy wrote:
> I think that depends on the C standard used. With C99, there's no rule
> for the padding bytes initialization.
> With C11, in 6.7.9 Initialization of the standard: "the remainder of
> the aggregate shall be initialized implicitly the same as objects that
> have static storage duration", and with static storage will "every
> member is initialized (recursively) according to these rules, and any
> padding is initialized to zero bits".

I don't think that rule applies for things like xl_running_xacts, as it does
not have static storage duration.

> So if I read this correctly, '{0}' will set padding bytes to 0 when
> using C11. But given Postgres is using C99, that's not something we
> can rely on?

We use C11, but the guarantee doesn't help us, due to the static storage
duration restriction. However, in C23, this has been fixed:

6.7.10 Initialization, point 11:

If an object that has automatic storage duration is initialized with an empty
initializer, its value is the same as the initialization of a static storage
duration object. Otherwise, if an object that has automatic storage duration
is not initialized explicitly, its representation is indeterminate. [...]

This notably includes being able to initialize everything to default with {}.

But C23 won't help us for a while :(

> > > True about the initialization part, mostly I guess, still we tend to
> > > worry about eliminating padding because these are wasted bytes in the
> > > WAL records.  For example, xlhp_freeze_plans has two bytes of padding,
> > > that we eliminate while inserting its record by splitting the
> > > FLEXIBLE_ARRAY_MEMBER part.
> >
> > But in the case of this thread it's in the middle of the struct, so I'm not
> > sure the "wasted" bytes would be elminated, would it?
>
> Moving subxid_overflow before xids, wouldn't you have 3 bytes of
> padding at the end of the struct for the whole struct alignment?

Yes.  I'm a bit doubtful the space wastage argument is strong for most of the
record types with padding, for a lot of them the waste through the padding is
such a small amount compared to the record type that it won't matter.

I don't think it makes a whole lot of sense to tackle this specifically for
xl_running_xacts. Until now we just accepted that WAL insertions can contain
random padding. If we don't want that, we should go around and make sure that
there is no padding (or padding is initialized) for *all* WAL records,
document that as the rule, and remove the relevant valgrind suppressions.

A lot of the WAL structs have holes. At least
- xl_brin_update
- xl_btree_mark_page_halfdead
- xl_btree_unlink_page
- xl_hash_vacuum_one_page
- xl_heap_inplace
- xl_heap_multi_insert
- xl_heap_rewrite_mapping
- xl_heap_truncate
- xl_invalidations
- xl_logical_message
- xl_multixact_create
- xl_running_xacts
- xl_xact_prepare
- xlhp_freeze_plan (not a toplevel type)
- xlhp_freeze_plans (not a toplevel type)

I didn't check how many WAL record have trailing padding that we don't avoid
with
  offsetoff(structname, last_field) + sizeof(last_field_type)
style hackery.

Greetings,

Andres Freund

---

From: Alexander Kuzmenkov <[email-masked]>
Date: Tue, 10 Mar 2026 22:51:50 +0100

> document that as the rule, and remove the relevant valgrind suppressions.

That's not random, that's server memory, right? Probably not another 
Heartbleed, but I'd rather initialize a few locals than find out.

Happy to see this being worked on, these uninitialized WAL records are a 
major obstacle to enabling MemorySanitizer. I ran into this again today 
and this is how I found this thread. Unfortunately the MemorySanitizer 
can't even use the same suppressions as Valgrind, because the 
suppression architecture is different (can only remove the checks from a 
given function, not all stack traces that have this function like 
Valgrind does).

Best regards
Alexander Kuzmenkov
TigerData

---

From: Heikki Linnakangas <[email-masked]>
Date: Wed, 11 Mar 2026 00:09:14 +0200

> given function, not all stack traces that have this function like 

+1 for initializing all padding in WAL records. In fact I thought that 
we already did that. (Except in this case, apparently)

- Heikki

---

From: Alexander Kuzmenkov <[email-masked]>
Date: Wed, 11 Mar 2026 11:45:28 +0100

> we already did that. (Except in this case, apparently)

I found 42 exceptions like this. See the attached patch, it
initializes some WAL records and removes the WAL-related Valgrind
suppressions. The regression tests pass under Valgrind with these
changes.

As discussed above, I used memset instead of = { 0 }. I could observe
the latter to not initialize the padding on some configurations.
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 6afd5367c59..261405a122f 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -1138,6 +1138,8 @@ brinbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 		XLogRecPtr	recptr;
 		Page		page;
 
+		memset(&xlrec, 0, sizeof(xlrec));
+
 		xlrec.version = BRIN_CURRENT_VERSION;
 		xlrec.pagesPerRange = BrinGetPagesPerRange(index);
 
diff --git a/src/backend/access/brin/brin_pageops.c b/src/backend/access/brin/brin_pageops.c
index a5187a6202f..bdef9a01fbd 100644
--- a/src/backend/access/brin/brin_pageops.c
+++ b/src/backend/access/brin/brin_pageops.c
@@ -187,6 +187,8 @@ brin_doupdate(Relation idxrel, BlockNumber pagesPerRange,
 			XLogRecPtr	recptr;
 			uint8		info = XLOG_BRIN_SAMEPAGE_UPDATE;
 
+			memset(&xlrec, 0, sizeof(xlrec));
+
 			xlrec.offnum = oldoff;
 
 			XLogBeginInsert();
@@ -272,6 +274,8 @@ brin_doupdate(Relation idxrel, BlockNumber pagesPerRange,
 			XLogRecPtr	recptr;
 			uint8		info;
 
+			memset(&xlrec, 0, sizeof(xlrec));
+
 			info = XLOG_BRIN_UPDATE | (extended ? XLOG_BRIN_INIT_PAGE : 0);
 
 			xlrec.insert.offnum = newoff;
@@ -429,6 +433,8 @@ brin_doinsert(Relation idxrel, BlockNumber pagesPerRange,
 		XLogRecPtr	recptr;
 		uint8		info;
 
+		memset(&xlrec, 0, sizeof(xlrec));
+
 		info = XLOG_BRIN_INSERT | (extended ? XLOG_BRIN_INIT_PAGE : 0);
 		xlrec.heapBlk = heapBlk;
 		xlrec.pagesPerRange = pagesPerRange;
diff --git a/src/backend/access/brin/brin_revmap.c b/src/backend/access/brin/brin_revmap.c
index 4e380ecc710..96d1795a6d3 100644
--- a/src/backend/access/brin/brin_revmap.c
+++ b/src/backend/access/brin/brin_revmap.c
@@ -411,6 +411,8 @@ brinRevmapDesummarizeRange(Relation idxrel, BlockNumber heapBlk)
 		xl_brin_desummarize xlrec;
 		XLogRecPtr	recptr;
 
+		memset(&xlrec, 0, sizeof(xlrec));
+
 		xlrec.pagesPerRange = revmap->rm_pagesPerRange;
 		xlrec.heapBlk = heapBlk;
 		xlrec.regOffset = regOffset;
@@ -624,6 +626,8 @@ revmap_physical_extend(BrinRevmap *revmap)
 		xl_brin_revmap_extend xlrec;
 		XLogRecPtr	recptr;
 
+		memset(&xlrec, 0, sizeof(xlrec));
+
 		xlrec.targetBlk = mapBlk;
 
 		XLogBeginInsert();
diff --git a/src/backend/access/gin/ginbtree.c b/src/backend/access/gin/ginbtree.c
index 644d484ea53..189b579c60e 100644
--- a/src/backend/access/gin/ginbtree.c
+++ b/src/backend/access/gin/ginbtree.c
@@ -421,6 +421,8 @@ ginPlaceToPage(GinBtree btree, GinBtreeStack *stack,
 			ginxlogInsert xlrec;
 			BlockIdData childblknos[2];
 
+			memset(&xlrec, 0, sizeof(xlrec));
+
 			xlrec.flags = xlflags;
 
 			XLogRegisterData(&xlrec, sizeof(ginxlogInsert));
@@ -461,6 +463,8 @@ ginPlaceToPage(GinBtree btree, GinBtreeStack *stack,
 		Buffer		lbuffer = InvalidBuffer;
 		Page		newrootpg = NULL;
 
+		memset(&data, 0, sizeof(data));
+
 		/* Get a new index page to become the right page */
 		rbuffer = GinNewBuffer(btree->index);
 
diff --git a/src/backend/access/gin/ginfast.c b/src/backend/access/gin/ginfast.c
index a6d88572cc2..fd1c46bc20f 100644
--- a/src/backend/access/gin/ginfast.c
+++ b/src/backend/access/gin/ginfast.c
@@ -118,6 +118,8 @@ writeListPage(Relation index, Buffer buffer,
 		ginxlogInsertListPage data;
 		XLogRecPtr	recptr;
 
+		memset(&data, 0, sizeof(data));
+
 		data.rightlink = rightlink;
 		data.ntuples = ntuples;
 
@@ -230,6 +232,8 @@ ginHeapTupleFastInsert(GinState *ginstate, GinTupleCollector *collector)
 	int			cleanupSize;
 	bool		needWal;
 
+	memset(&data, 0, sizeof(data));
+
 	if (collector->ntuples == 0)
 		return;
 
@@ -571,6 +575,8 @@ shiftList(Relation index, Buffer metabuffer, BlockNumber newHead,
 		Buffer		buffers[GIN_NDELETE_AT_ONCE];
 		BlockNumber freespace[GIN_NDELETE_AT_ONCE];
 
+		memset(&data, 0, sizeof(data));
+
 		data.ndeleted = 0;
 		while (data.ndeleted < GIN_NDELETE_AT_ONCE && blknoToDelete != newHead)
 		{
diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c
index 78f7b7a2495..2c85e31dc1a 100644
--- a/src/backend/access/gin/ginutil.c
+++ b/src/backend/access/gin/ginutil.c
@@ -687,6 +687,8 @@ ginUpdateStats(Relation index, const GinStatsData *stats, bool is_build)
 		XLogRecPtr	recptr;
 		ginxlogUpdateMeta data;
 
+		memset(&data, 0, sizeof(data));
+
 		data.locator = index->rd_locator;
 		data.ntuples = 0;
 		data.newRightlink = data.prevTail = InvalidBlockNumber;
diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c
index fbbe3a6dd70..c0d4fb86b48 100644
--- a/src/backend/access/gin/ginvacuum.c
+++ b/src/backend/access/gin/ginvacuum.c
@@ -199,6 +199,8 @@ ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkn
 		XLogRecPtr	recptr;
 		ginxlogDeletePage data;
 
+		memset(&data, 0, sizeof(data));
+
 		/*
 		 * We can't pass REGBUF_STANDARD for the deleted page, because we
 		 * didn't set pd_lower on pre-9.4 versions. The page might've been
diff --git a/src/backend/access/gist/gistxlog.c b/src/backend/access/gist/gistxlog.c
index b354e4ba5d1..6b4cab9de16 100644
--- a/src/backend/access/gist/gistxlog.c
+++ b/src/backend/access/gist/gistxlog.c
@@ -503,6 +503,8 @@ gistXLogSplit(bool page_is_leaf,
 	XLogRecPtr	recptr;
 	int			i;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	for (ptr = dist; ptr; ptr = ptr->next)
 		npage++;
 
@@ -555,6 +557,8 @@ gistXLogPageDelete(Buffer buffer, FullTransactionId xid,
 	gistxlogPageDelete xlrec;
 	XLogRecPtr	recptr;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	xlrec.deleteXid = xid;
 	xlrec.downlinkOffset = downlinkOffset;
 
@@ -635,6 +639,8 @@ gistXLogUpdate(Buffer buffer,
 	int			i;
 	XLogRecPtr	recptr;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	xlrec.ntodelete = ntodelete;
 	xlrec.ntoinsert = ituplen;
 
@@ -673,6 +679,8 @@ gistXLogDelete(Buffer buffer, OffsetNumber *todelete, int ntodelete,
 	gistxlogDelete xlrec;
 	XLogRecPtr	recptr;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(heaprel);
 	xlrec.snapshotConflictHorizon = snapshotConflictHorizon;
 	xlrec.ntodelete = ntodelete;
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 6203e3d7f8d..daab44ad937 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -2366,6 +2366,8 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples,
 	int			npages = 0;
 	int			npages_used = 0;
 
+	memset(&scratch, 0, sizeof(scratch));
+
 	/* currently not needed (thus unsupported) for heap_multi_insert() */
 	Assert(!(options & HEAP_INSERT_NO_LOGICAL));
 
@@ -6587,6 +6589,7 @@ heap_inplace_update_and_unlock(Relation relation,
 		BlockNumber blkno;
 		XLogRecPtr	recptr;
 
+		memset(&xlrec, 0, sizeof(xlrec));
 		xlrec.offnum = ItemPointerGetOffsetNumber(&tuple->t_self);
 		xlrec.dbId = MyDatabaseId;
 		xlrec.tsId = MyDatabaseTableSpace;
diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index a8025889be0..a6c918612c0 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -1957,6 +1957,7 @@ heap_log_freeze_cmp(const void *arg1, const void *arg2)
 static inline void
 heap_log_freeze_new_plan(xlhp_freeze_plan *plan, HeapTupleFreeze *frz)
 {
+	memset(plan, 0, sizeof(*plan));
 	plan->xmax = frz->xmax;
 	plan->t_infomask2 = frz->t_infomask2;
 	plan->t_infomask = frz->t_infomask;
@@ -2071,7 +2072,8 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer,
 	xlhp_prune_items unused_items;
 	OffsetNumber frz_offsets[MaxHeapTuplesPerPage];
 
-	xlrec.flags = 0;
+	memset(&xlrec, 0, sizeof(xlrec));
+	memset(&freeze_plans, 0, sizeof(freeze_plans));
 
 	/*
 	 * Prepare data for the buffer.  The arrays are not actually in the
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index aa82cede30a..ff475aa9739 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -1317,6 +1317,8 @@ _bt_insertonpg(Relation rel,
 			XLogRecPtr	recptr;
 			uint16		upostingoff;
 
+			memset(&xlrec, 0, sizeof(xlrec));
+			memset(&xlmeta, 0, sizeof(xlmeta));
 			xlrec.offnum = newitemoff;
 
 			XLogBeginInsert();
@@ -1970,6 +1972,7 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf,
 		uint8		xlinfo;
 		XLogRecPtr	recptr;
 
+		memset(&xlrec, 0, sizeof(xlrec));
 		xlrec.level = ropaque->btpo_level;
 		/* See comments below on newitem, orignewitem, and posting lists */
 		xlrec.firstrightoff = firstrightoff;
@@ -2560,6 +2563,8 @@ _bt_newlevel(Relation rel, Relation heaprel, Buffer lbuf, Buffer rbuf)
 		XLogRecPtr	recptr;
 		xl_btree_metadata md;
 
+		memset(&xlrec, 0, sizeof(xlrec));
+		memset(&md, 0, sizeof(md));
 		xlrec.rootblk = rootblknum;
 		xlrec.level = metad->btm_level;
 
diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c
index c79dd38ee18..89f92dc7cba 100644
--- a/src/backend/access/nbtree/nbtpage.c
+++ b/src/backend/access/nbtree/nbtpage.c
@@ -287,6 +287,7 @@ _bt_set_cleanup_info(Relation rel, BlockNumber num_delpages)
 		xl_btree_metadata md;
 		XLogRecPtr	recptr;
 
+		memset(&md, 0, sizeof(md));
 		XLogBeginInsert();
 		XLogRegisterBuffer(0, metabuf, REGBUF_WILL_INIT | REGBUF_STANDARD);
 
@@ -475,6 +476,8 @@ _bt_getroot(Relation rel, Relation heaprel, int access)
 			XLogRecPtr	recptr;
 			xl_btree_metadata md;
 
+			memset(&xlrec, 0, sizeof(xlrec));
+			memset(&md, 0, sizeof(md));
 			XLogBeginInsert();
 			XLogRegisterBuffer(0, rootbuf, REGBUF_WILL_INIT);
 			XLogRegisterBuffer(2, metabuf, REGBUF_WILL_INIT | REGBUF_STANDARD);
@@ -2253,6 +2256,7 @@ _bt_mark_page_halfdead(Relation rel, Relation heaprel, Buffer leafbuf,
 		xl_btree_mark_page_halfdead xlrec;
 		XLogRecPtr	recptr;
 
+		memset(&xlrec, 0, sizeof(xlrec));
 		xlrec.poffset = poffset;
 		xlrec.leafblk = leafblkno;
 		if (topparent != leafblkno)
@@ -2674,6 +2678,8 @@ _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno,
 		uint8		xlinfo;
 		XLogRecPtr	recptr;
 
+		memset(&xlrec, 0, sizeof(xlrec));
+		memset(&xlmeta, 0, sizeof(xlmeta));
 		XLogBeginInsert();
 
 		XLogRegisterBuffer(0, buf, REGBUF_WILL_INIT);
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index af6b27b2135..7458715907f 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -205,6 +205,8 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 {
 	spgxlogAddLeaf xlrec;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	xlrec.newPage = isNew;
 	xlrec.storesNulls = isNulls;
 
@@ -404,6 +406,8 @@ moveLeafs(Relation index, SpGistState *state,
 	char	   *leafdata,
 			   *leafptr;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	/* This doesn't work on root page */
 	Assert(parent->buffer != InvalidBuffer);
 	Assert(parent->buffer != current->buffer);
@@ -714,6 +718,8 @@ doPickSplit(Relation index, SpGistState *state,
 				nToInsert,
 				maxToInclude;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	in.level = level;
 
 	/*
@@ -1518,6 +1524,8 @@ spgAddNodeAction(Relation index, SpGistState *state,
 	SpGistInnerTuple newInnerTuple;
 	spgxlogAddNode xlrec;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	/* Should not be applied to nulls */
 	Assert(!SpGistPageStoresNulls(current->page));
 
@@ -1726,6 +1734,8 @@ spgSplitNodeAction(Relation index, SpGistState *state,
 	spgxlogSplitTuple xlrec;
 	Buffer		newBuffer = InvalidBuffer;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	/* Should not be applied to nulls */
 	Assert(!SpGistPageStoresNulls(current->page));
 
diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c
index 2678f7ab782..7d853bc9c07 100644
--- a/src/backend/access/spgist/spgvacuum.c
+++ b/src/backend/access/spgist/spgvacuum.c
@@ -140,6 +140,7 @@ vacuumLeafPage(spgBulkDeleteState *bds, Relation index, Buffer buffer,
 	OffsetNumber i,
 				max = PageGetMaxOffsetNumber(page);
 
+	memset(&xlrec, 0, sizeof(xlrec));
 	memset(predecessor, 0, sizeof(predecessor));
 	memset(deletable, 0, sizeof(deletable));
 	nDeletable = 0;
@@ -414,6 +415,8 @@ vacuumLeafRoot(spgBulkDeleteState *bds, Relation index, Buffer buffer)
 	OffsetNumber i,
 				max = PageGetMaxOffsetNumber(page);
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	xlrec.nDelete = 0;
 
 	/* Scan page, identify tuples to delete, accumulate stats */
@@ -505,6 +508,8 @@ vacuumRedirectAndPlaceholder(Relation index, Relation heaprel, Buffer buffer)
 	spgxlogVacuumRedirect xlrec;
 	GlobalVisState *vistest;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(heaprel);
 	xlrec.nToPlaceholder = 0;
 	xlrec.snapshotConflictHorizon = InvalidTransactionId;
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index 7fa8d9247e0..a849c494251 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1355,6 +1355,8 @@ LogCurrentRunningXacts(RunningTransactions CurrRunningXacts)
 	xl_running_xacts xlrec;
 	XLogRecPtr	recptr;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	xlrec.xcnt = CurrRunningXacts->xcnt;
 	xlrec.subxcnt = CurrRunningXacts->subxcnt;
 	xlrec.subxid_overflow = (CurrRunningXacts->subxid_status != SUBXIDS_IN_ARRAY);
diff --git a/src/tools/valgrind.supp b/src/tools/valgrind.supp
index 2ad5b81526d..dc2e1323626 100644
--- a/src/tools/valgrind.supp
+++ b/src/tools/valgrind.supp
@@ -23,23 +23,6 @@
 	fun:pgstat_write_statsfiles
 }
 
-{
-	padding_XLogRecData_CRC
-	Memcheck:Value8
-
-	fun:pg_comp_crc32c*
-	fun:XLogRecordAssemble
-}
-
-{
-	padding_XLogRecData_write
-	Memcheck:Param
-	pwrite64(buf)
-
-	...
-	fun:XLogWrite
-}
-
 {
 	padding_relcache
 	Memcheck:Param

---

From: Alexander Kuzmenkov <[email-masked]>
Date: Wed, 11 Mar 2026 12:07:58 +0100

> suppressions. The regression tests pass under Valgrind with these

I think I'm making some unneeded changes here though. For example in
ginxlogInsertListPage for a two-int struct with no padding. I'll need
to check them again one by one.

---

From: Heikki Linnakangas <[email-masked]>
Date: Thu, 12 Mar 2026 14:54:52 +0200

> to check them again one by one.

I experimented with this a little more. Valgrind complained about one 
more place on 'master': the xl_multixact_create got padding, when 
MultiXactOffset was widened to 64 bits. That could be fixed by swapping 
the fields.

Another thing I did to find possible initializations: I ran 'pahole 
bin/postgres' and search for all the "xl_*" structs with padding, and 
then looked at where they're initialized. Attached patch (0003) shows a 
few places that look suspicious to me. I don't think I caught all 
structs used in WAL records, though, like the ginxlogInsertListPage 
thing mentioned.

I wish we could just mark all WAL record structs with 
pg_attribute_packed(). Unfortunately pg_attribute_packed() is not 
available on all compilers we support.

- Heikki
From 2b9a360952aae94eb09afd7a46b0e4170f916464 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email-masked]>
Date: Thu, 12 Mar 2026 12:42:11 +0200
Subject: [PATCH 1/4] Initialize WAL record structs

Author: Alexander Kuzmenkov <[email-masked]>
---
 src/backend/access/brin/brin.c          |  2 ++
 src/backend/access/brin/brin_pageops.c  |  6 ++++++
 src/backend/access/brin/brin_revmap.c   |  4 ++++
 src/backend/access/gin/ginbtree.c       |  4 ++++
 src/backend/access/gin/ginfast.c        |  6 ++++++
 src/backend/access/gin/ginutil.c        |  2 ++
 src/backend/access/gin/ginvacuum.c      |  2 ++
 src/backend/access/gist/gistxlog.c      |  8 ++++++++
 src/backend/access/heap/heapam.c        |  3 +++
 src/backend/access/heap/pruneheap.c     |  5 ++++-
 src/backend/access/nbtree/nbtinsert.c   |  5 +++++
 src/backend/access/nbtree/nbtpage.c     |  6 ++++++
 src/backend/access/spgist/spgdoinsert.c | 10 ++++++++++
 src/backend/access/spgist/spgvacuum.c   |  5 +++++
 src/backend/storage/ipc/standby.c       |  2 ++
 src/tools/valgrind.supp                 | 17 -----------------
 16 files changed, 69 insertions(+), 18 deletions(-)

diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 1909c3254b5..88e32c7fb74 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -1141,6 +1141,8 @@ brinbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 		XLogRecPtr	recptr;
 		Page		page;

index 7da97bec43b..dc56932ab21 100644

 			XLogBeginInsert();
@@ -271,6 +273,8 @@ brin_doupdate(Relation idxrel, BlockNumber pagesPerRange,
 			XLogRecPtr	recptr;
 			uint8		info;

 			xlrec.insert.offnum = newoff;
@@ -427,6 +431,8 @@ brin_doinsert(Relation idxrel, BlockNumber pagesPerRange,
 		XLogRecPtr	recptr;
 		uint8		info;
 
index 233355cb2d5..c9db4e59822 100644

 
 		XLogBeginInsert();
index 3d3a9da56b1..6eaeebf79bc 100644

 
 
index f50848eb65a..710c16887df 100644

 

+		memset(&data, 0, sizeof(data));
+
 		data.ndeleted = 0;
 		while (data.ndeleted < GIN_NDELETE_AT_ONCE && blknoToDelete != newHead)
 		{
diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c
index ff927279cc3..76be56e932b 100644
--- a/src/backend/access/gin/ginutil.c
+++ b/src/backend/access/gin/ginutil.c
@@ -650,6 +650,8 @@ ginUpdateStats(Relation index, const GinStatsData *stats, bool is_build)
 		XLogRecPtr	recptr;
 		ginxlogUpdateMeta data;
 
+		memset(&data, 0, sizeof(data));
+
 		data.locator = index->rd_locator;
 		data.ntuples = 0;
 		data.newRightlink = data.prevTail = InvalidBlockNumber;
diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c
index d5c8bef5ceb..ad3c4115593 100644
--- a/src/backend/access/gin/ginvacuum.c
+++ b/src/backend/access/gin/ginvacuum.c
@@ -200,6 +200,8 @@ ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkn
 		XLogRecPtr	recptr;
 		ginxlogDeletePage data;
 
+		memset(&data, 0, sizeof(data));
+
 		/*
 		 * We can't pass REGBUF_STANDARD for the deleted page, because we
 		 * didn't set pd_lower on pre-9.4 versions. The page might've been
diff --git a/src/backend/access/gist/gistxlog.c b/src/backend/access/gist/gistxlog.c
index c783838495f..8629514e265 100644
--- a/src/backend/access/gist/gistxlog.c
+++ b/src/backend/access/gist/gistxlog.c
@@ -501,6 +501,8 @@ gistXLogSplit(bool page_is_leaf,
 	XLogRecPtr	recptr;
 	int			i;

@@ -553,6 +555,8 @@ gistXLogPageDelete(Buffer buffer, FullTransactionId xid,
 	gistxlogPageDelete xlrec;
 	XLogRecPtr	recptr;

@@ -633,6 +637,8 @@ gistXLogUpdate(Buffer buffer,
 	int			i;
 	XLogRecPtr	recptr;

@@ -671,6 +677,8 @@ gistXLogDelete(Buffer buffer, OffsetNumber *todelete, int ntodelete,
 	gistxlogDelete xlrec;
 	XLogRecPtr	recptr;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(heaprel);
 	xlrec.snapshotConflictHorizon = snapshotConflictHorizon;
 	xlrec.ntodelete = ntodelete;
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 8f1c11a9350..b11dba81aef 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -2428,6 +2428,8 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples,
 	int			npages = 0;
 	int			npages_used = 0;

@@ -6654,6 +6656,7 @@ heap_inplace_update_and_unlock(Relation relation,
 		BlockNumber blkno;
 		XLogRecPtr	recptr;
 
+		memset(&xlrec, 0, sizeof(xlrec));
 		xlrec.offnum = ItemPointerGetOffsetNumber(&tuple->t_self);
 		xlrec.dbId = MyDatabaseId;
 		xlrec.tsId = MyDatabaseTableSpace;
diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index 6beeb6956e3..54b4a5ab0c8 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -2058,6 +2058,7 @@ heap_log_freeze_cmp(const void *arg1, const void *arg2)
 static inline void
 heap_log_freeze_new_plan(xlhp_freeze_plan *plan, HeapTupleFreeze *frz)
 {
+	memset(plan, 0, sizeof(*plan));
 	plan->xmax = frz->xmax;
 	plan->t_infomask2 = frz->t_infomask2;
 	plan->t_infomask = frz->t_infomask;
@@ -2182,7 +2183,9 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer,
 
 	Assert((vmflags & VISIBILITYMAP_VALID_BITS) == vmflags);
 
-	xlrec.flags = 0;
+	memset(&xlrec, 0, sizeof(xlrec));
+	memset(&freeze_plans, 0, sizeof(freeze_plans));
+
 	regbuf_flags_heap = REGBUF_STANDARD;
 
 	/*
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index 796e1513ddf..a4a6d4e8995 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -1336,6 +1336,8 @@ _bt_insertonpg(Relation rel,
 			XLogRecPtr	recptr;
 			uint16		upostingoff;

 			XLogBeginInsert();
@@ -1996,6 +1998,7 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf,
 		uint8		xlinfo;
 		XLogRecPtr	recptr;
 
@@ -2585,6 +2588,8 @@ _bt_newlevel(Relation rel, Relation heaprel, Buffer lbuf, Buffer rbuf)
 		XLogRecPtr	recptr;
 		xl_btree_metadata md;

diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c
index 4125c185e8b..176090e6558 100644
--- a/src/backend/access/nbtree/nbtpage.c
+++ b/src/backend/access/nbtree/nbtpage.c
@@ -288,6 +288,7 @@ _bt_set_cleanup_info(Relation rel, BlockNumber num_delpages)
 		xl_btree_metadata md;
 		XLogRecPtr	recptr;

@@ -476,6 +477,8 @@ _bt_getroot(Relation rel, Relation heaprel, int access)
 			XLogRecPtr	recptr;
 			xl_btree_metadata md;
 
@@ -2255,6 +2258,7 @@ _bt_mark_page_halfdead(Relation rel, Relation heaprel, Buffer leafbuf,
 
+		memset(&xlrec, 0, sizeof(xlrec));
 		xlrec.poffset = poffset;
 		xlrec.leafblk = leafblkno;
 		if (topparent != leafblkno)
@@ -2678,6 +2682,8 @@ _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno,
 		uint8		xlinfo;
 		XLogRecPtr	recptr;

 		XLogRegisterBuffer(0, buf, REGBUF_WILL_INIT);
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 7c7371c69e8..e96b20c30bd 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -204,6 +204,8 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple,
 {
 	spgxlogAddLeaf xlrec;

@@ -403,6 +405,8 @@ moveLeafs(Relation index, SpGistState *state,
 	char	   *leafdata,
 			   *leafptr;
 
@@ -710,6 +714,8 @@ doPickSplit(Relation index, SpGistState *state,
 				nToInsert,
 				maxToInclude;

 	/*
@@ -1514,6 +1520,8 @@ spgAddNodeAction(Relation index, SpGistState *state,
 	SpGistInnerTuple newInnerTuple;
 	spgxlogAddNode xlrec;

@@ -1722,6 +1730,8 @@ spgSplitNodeAction(Relation index, SpGistState *state,
 	spgxlogSplitTuple xlrec;
 	Buffer		newBuffer = InvalidBuffer;

index 6b7117b56b2..9e6acb873ef 100644

 
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(heaprel);
 	xlrec.nToPlaceholder = 0;
 	xlrec.snapshotConflictHorizon = InvalidTransactionId;
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index f3ad90c7c7a..4ae5be2792b 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1357,6 +1357,8 @@ LogCurrentRunningXacts(RunningTransactions CurrRunningXacts)
 	xl_running_xacts xlrec;
 	XLogRecPtr	recptr;
 
index d56794b4f7e..f2ab0c8a052 100644
 
-{
-- 
2.47.3

From 74d0084faef3bc6d748bd1e26e39c2683d27de1a Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email-masked]>
Date: Thu, 12 Mar 2026 14:30:13 +0200
Subject: [PATCH 2/4] Avoid padding in xl_multixact_create WAL record

When MultiXactOffset was widened to 64 bits in commit bd8d9c9bdf, the
struct started to need alignment padding for the 'moff' field. Reorder
the fields to avoid it.

Discussion: https://www.postgresql.org/message-id/CALzhyqzKTRVsQGj+qDDRVs3Oo0EvffuQvVO0v4rbpWU=[email-masked]
---
 src/include/access/multixact.h     | 2 +-
 src/include/access/xlog_internal.h | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/include/access/multixact.h b/src/include/access/multixact.h
index 2ae8b571dcc..757c631e725 100644
--- a/src/include/access/multixact.h
+++ b/src/include/access/multixact.h
@@ -72,8 +72,8 @@ typedef struct MultiXactMember
 typedef struct xl_multixact_create
 {
 	MultiXactId mid;			/* new MultiXact's ID */
-	MultiXactOffset moff;		/* its starting offset in members file */
 	int32		nmembers;		/* number of member XIDs */
+	MultiXactOffset moff;		/* its starting offset in members file */
 	MultiXactMember members[FLEXIBLE_ARRAY_MEMBER];
 } xl_multixact_create;
 
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 58ae12bb20f..629ac3a7d3e 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -31,7 +31,7 @@
 /*
  * Each page of XLOG file has a header like this:
  */
-#define XLOG_PAGE_MAGIC 0xD11C	/* can be used as WAL version indicator */
+#define XLOG_PAGE_MAGIC 0xD11D	/* can be used as WAL version indicator */
 
 typedef struct XLogPageHeaderData
 {
-- 
2.47.3

From a60e3cb4911911fff968eadb4e0f318c2edd1dbb Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email-masked]>
Date: Thu, 12 Mar 2026 14:40:25 +0200
Subject: [PATCH 3/4] XXX: a few more places that maybe need clearing?

I spotted these by doing 'pahole bin/postgres', and searching for
"xl_*" structs that have padding in them. Then I searched where
they're initialized.
---
 src/backend/access/hash/hashinsert.c      | 2 ++
 src/backend/access/heap/heapam.c          | 6 ++++++
 src/backend/access/heap/rewriteheap.c     | 2 ++
 src/backend/access/transam/twophase.c     | 2 ++
 src/backend/commands/tablecmds.c          | 2 ++
 src/backend/replication/logical/message.c | 2 ++
 6 files changed, 16 insertions(+)

diff --git a/src/backend/access/hash/hashinsert.c b/src/backend/access/hash/hashinsert.c
index 0cefbacc96e..45e613ceddb 100644
--- a/src/backend/access/hash/hashinsert.c
+++ b/src/backend/access/hash/hashinsert.c
@@ -426,6 +426,8 @@ _hash_vacuum_one_page(Relation rel, Relation hrel, Buffer metabuf, Buffer buf)
 			xl_hash_vacuum_one_page xlrec;
 			XLogRecPtr	recptr;
 
+			/* XXX: clear padding? */
+
 			xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(hrel);
 			xlrec.snapshotConflictHorizon = snapshotConflictHorizon;
 			xlrec.ntuples = ndeletable;
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index b11dba81aef..54943be6126 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -2616,6 +2616,12 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples,
 			xlrec = (xl_heap_multi_insert *) scratchptr;
 			scratchptr += SizeOfHeapMultiInsert;
 
+			/*
+			 * XXX: need to clear padding in xl_heap_multi_insert? The scratch
+			 * area was zeroed at the top of the function, but we're reusing
+			 * it for multiple records.
+			 */
+
 			/*
 			 * Allocate offsets array. Unless we're reinitializing the page,
 			 * in that case the tuples are stored in order starting at
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index 6b19ac3030d..b6d784572ec 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -842,6 +842,8 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
 		else
 			dboid = MyDatabaseId;
 
+		/* XXX: clear padding? */
+
 		xlrec.num_mappings = num_mappings;
 		xlrec.mapped_rel = RelationGetRelid(state->rs_old_rel);
 		xlrec.mapped_xid = src->xid;
diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 55b9f38927d..c22ab93965e 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -1076,6 +1076,8 @@ StartPrepare(GlobalTransaction gxact)
 
 	records.total_len = 0;
 
+	/* XXX: Clear padding? */
+
 	/* Create header */
 	hdr.magic = TWOPHASE_MAGIC;
 	hdr.total_len = 0;			/* EndPrepare will fill this in */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a7c32679b22..1bf59f77ae1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -2325,6 +2325,8 @@ ExecuteTruncateGuts(List *explicit_rels,
 		foreach(cell, relids_logged)
 			logrelids[i++] = lfirst_oid(cell);
 
+		/* XXX: clear padding? */
+
 		xlrec.dbId = MyDatabaseId;
 		xlrec.nrelids = list_length(relids_logged);
 		xlrec.flags = 0;
diff --git a/src/backend/replication/logical/message.c b/src/backend/replication/logical/message.c
index 06825d66e7f..40d032a4ffc 100644
--- a/src/backend/replication/logical/message.c
+++ b/src/backend/replication/logical/message.c
@@ -55,6 +55,8 @@ LogLogicalMessage(const char *prefix, const char *message, size_t size,
 		GetCurrentTransactionId();
 	}
 
+	/* XXX: clear padding? */
+
 	xlrec.dbId = MyDatabaseId;
 	xlrec.transactional = transactional;
 	/* trailing zero is critical; see logicalmsg_desc */
-- 
2.47.3

From f36c2e1a052ae30d6fbdc946f990ab079234e332 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email-masked]>
Date: Thu, 12 Mar 2026 12:52:41 +0200
Subject: [PATCH 4/4] Add an explicit valgrind check in XLogInsert() for
 uninitialized data

Even without this, Valgrind complains if uninitialized memory copied
into the WAL, when the WAL is flushed to disk. With this commit, it's
caught earlier, in XLogInsert(), which makes it more clear where the
uninitialized data is coming from.
---
 src/backend/access/transam/xlog.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index b9b678f3722..8eae29a3548 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -98,6 +98,7 @@
 #include "utils/guc_hooks.h"
 #include "utils/guc_tables.h"
 #include "utils/injection_point.h"
+#include "utils/memdebug.h"
 #include "utils/pgstat_internal.h"
 #include "utils/ps_status.h"
 #include "utils/relmapper.h"
@@ -1259,6 +1260,8 @@ CopyXLogRecordToWAL(int write_len, bool isLogSwitch, XLogRecData *rdata,
 		const char *rdata_data = rdata->data;
 		int			rdata_len = rdata->len;
 
+		VALGRIND_CHECK_MEM_IS_DEFINED(rdata_data, rdata_len);
+
 		while (rdata_len > freespace)
 		{
 			/*
-- 
2.47.3

---

From: Alexander Kuzmenkov <[email-masked]>
Date: Thu, 12 Mar 2026 19:23:31 +0100

The functions in the "0003" patch haven't surfaced in my "make
installcheck-parallel" runs with Valgrind, or the "make check" with
MemorySanitizer. However, I could hit most of them with some fuzzing. The
only exception was `xl_hash_vacuum_one_page` but that's probably also
triggerable.

I noticed that we also use `sizeof` in some WAL functions, so probably the
tail padding can also be written to WAL? For example, consider this:
(gdb) ptype/o gistxlogPageSplit
type = struct gistxlogPageSplit {
/*      0      |       4 */    BlockNumber origrlink;
/* XXX  4-byte hole      */
/*      8      |       8 */    GistNSN orignsn;
/*     16      |       1 */    _Bool origleaf;
/* XXX  1-byte hole      */
/*     18      |       2 */    uint16 npage;
/*     20      |       1 */    _Bool markfollowright;
/* XXX  3-byte padding   */

                               /* total size (bytes):   24 */
                             }

And then we do  XLogRegisterData((char *) &xlrec,
sizeof(gistxlogPageSplit));

In general, I'm wondering what our approach to this should be. Several
potential improvements were mentioned, but I think for now we could focus
on removing the Valgrind suppression. This is a meaningful improvement that
uses the existing test tools. Do we want to defensively zero-initialize
every case that seems to be potentially affected, i.e. written to WAL and
has holes/tail padding? That sounds cheap and simple and probably even
backportable. In the "0001" patch, there are several cases where no padding
goes into WAL, I can remove these. For example, the use of
xl_brin_createidx in brinbuild() does not have this problem.

---

From: Alexander Kuzmenkov <[email-masked]>
Date: Mon, 16 Mar 2026 17:14:10 +0100

I have removed the unnecessary memsets (for structs with no padding). With
these changes, and removing the two WAL-related suppressions, the make
installcheck under Valgrind passes. The second patch is a small addition to
the hash index test that exercises the "vacuum one page" path we discussed
above.
diff --git a/src/backend/access/brin/brin_pageops.c b/src/backend/access/brin/brin_pageops.c
index 7da97be..ba3b1c2 100644
--- a/src/backend/access/brin/brin_pageops.c
+++ b/src/backend/access/brin/brin_pageops.c
@@ -271,6 +271,8 @@ brin_doupdate(Relation idxrel, BlockNumber pagesPerRange,
 			XLogRecPtr	recptr;
 			uint8		info;

 			xlrec.insert.offnum = newoff;
diff --git a/src/backend/access/gin/ginbtree.c b/src/backend/access/gin/ginbtree.c
index 3d3a9da..0021db0 100644
--- a/src/backend/access/gin/ginbtree.c
+++ b/src/backend/access/gin/ginbtree.c
@@ -461,6 +461,8 @@ ginPlaceToPage(GinBtree btree, GinBtreeStack *stack,
 		Buffer		lbuffer = InvalidBuffer;
 		Page		newrootpg = NULL;

diff --git a/src/backend/access/gin/ginfast.c b/src/backend/access/gin/ginfast.c
index f50848e..d83aecb 100644
--- a/src/backend/access/gin/ginfast.c
+++ b/src/backend/access/gin/ginfast.c
@@ -230,6 +230,8 @@ ginHeapTupleFastInsert(GinState *ginstate, GinTupleCollector *collector)
 	int			cleanupSize;
 	bool		needWal;

@@ -571,6 +573,8 @@ shiftList(Relation index, Buffer metabuffer, BlockNumber newHead,
 		Buffer		buffers[GIN_NDELETE_AT_ONCE];
 		BlockNumber freespace[GIN_NDELETE_AT_ONCE];
 
index ff92727..76be56e 100644
 
+		memset(&data, 0, sizeof(data));
+
 		data.locator = index->rd_locator;
 		data.ntuples = 0;
 		data.newRightlink = data.prevTail = InvalidBlockNumber;
diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c
index 840543e..1c524de 100644
--- a/src/backend/access/gin/ginvacuum.c
+++ b/src/backend/access/gin/ginvacuum.c
@@ -214,6 +214,8 @@ ginDeletePostingPage(GinVacuumState *gvs, Buffer dBuffer, Buffer lBuffer,
 		XLogRecPtr	recptr;
 		ginxlogDeletePage data;
 
index c783838..1028db4 100644

@@ -671,6 +673,8 @@ gistXLogDelete(Buffer buffer, OffsetNumber *todelete, int ntodelete,
 	gistxlogDelete xlrec;
 	XLogRecPtr	recptr;
 
index 0cefbac..6f30bd0 100644
@@ -426,6 +426,7 @@ _hash_vacuum_one_page(Relation rel, Relation hrel, Buffer metabuf, Buffer buf)
 
index 8f1c11a..b11dba8 100644

 
index 6beeb69..54b4a5a 100644

-	xlrec.flags = 0;
 
 	/*
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index 6b19ac3..6c94ae1 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -842,6 +842,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
 		else
 			dboid = MyDatabaseId;
 
+		memset(&xlrec, 0, sizeof(xlrec));
 		xlrec.num_mappings = num_mappings;
 		xlrec.mapped_rel = RelationGetRelid(state->rs_old_rel);
 		xlrec.mapped_xid = src->xid;
diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c
index 91bb37d..87ac75b 100644
--- a/src/backend/access/nbtree/nbtinsert.c
+++ b/src/backend/access/nbtree/nbtinsert.c
@@ -1337,6 +1337,7 @@ _bt_insertonpg(Relation rel,
 			XLogRecPtr	recptr;
 			uint16		upostingoff;

 			XLogBeginInsert();
@@ -2602,6 +2603,7 @@ _bt_newlevel(Relation rel, Relation heaprel, Buffer lbuf, Buffer rbuf)
 		XLogRecPtr	recptr;
 		xl_btree_metadata md;

index 9aa7806..1fb6ced 100644

@@ -476,6 +477,7 @@ _bt_getroot(Relation rel, Relation heaprel, int access)
 			XLogRecPtr	recptr;
 			xl_btree_metadata md;
 
@@ -2255,6 +2257,7 @@ _bt_mark_page_halfdead(Relation rel, Relation heaprel, Buffer leafbuf,
 
+		memset(&xlrec, 0, sizeof(xlrec));
 		xlrec.poffset = poffset;
 		xlrec.leafblk = leafblkno;
 		if (topparent != leafblkno)
@@ -2678,6 +2681,8 @@ _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno,
 		uint8		xlinfo;
 		XLogRecPtr	recptr;

 		XLogRegisterBuffer(0, buf, REGBUF_WILL_INIT);
diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c
index 7c7371c..4cc4768 100644
--- a/src/backend/access/spgist/spgdoinsert.c
+++ b/src/backend/access/spgist/spgdoinsert.c
@@ -403,6 +403,8 @@ moveLeafs(Relation index, SpGistState *state,
 	char	   *leafdata,
 			   *leafptr;
 
@@ -710,6 +712,8 @@ doPickSplit(Relation index, SpGistState *state,
 				nToInsert,
 				maxToInclude;

 	/*
@@ -1514,6 +1518,8 @@ spgAddNodeAction(Relation index, SpGistState *state,
 	SpGistInnerTuple newInnerTuple;
 	spgxlogAddNode xlrec;

index 6b7117b..9e6acb8 100644

 
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(heaprel);
 	xlrec.nToPlaceholder = 0;
 	xlrec.snapshotConflictHorizon = InvalidTransactionId;
diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 55b9f38..4406e34 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -1040,6 +1040,9 @@ save_state_data(const void *data, uint32 len)
 	}
 
 	memcpy(records.tail->data + records.tail->len, data, len);
+	/* Zero MAXALIGN padding to avoid uninitialized bytes in WAL */
+	if (padlen > len)
+		memset(records.tail->data + records.tail->len + len, 0, padlen - len);
 	records.tail->len += padlen;
 	records.bytes_free -= padlen;
 	records.total_len += padlen;
@@ -1077,6 +1080,7 @@ StartPrepare(GlobalTransaction gxact)
 	records.total_len = 0;
 
 	/* Create header */
+	memset(&hdr, 0, sizeof(hdr));
 	hdr.magic = TWOPHASE_MAGIC;
 	hdr.total_len = 0;			/* EndPrepare will fill this in */
 	hdr.xid = xid;
@@ -1275,6 +1279,7 @@ RegisterTwoPhaseRecord(TwoPhaseRmgrId rmid, uint16 info,
 {
 	TwoPhaseRecordOnDisk record;
 
+	memset(&record, 0, sizeof(record));
 	record.rmid = rmid;
 	record.info = info;
 	record.len = len;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index b9b678f..f5b3616 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -5149,6 +5149,7 @@ BootStrapXLOG(uint32 data_checksum_version)
 	 * segment with logid=0 logseg=1. The very first WAL segment, 0/0, is not
 	 * used, so that we can use 0/0 to mean "before any valid WAL segment".
 	 */
+	memset(&checkPoint, 0, sizeof(checkPoint));
 	checkPoint.redo = wal_segment_size + SizeOfXLogLongPHD;
 	checkPoint.ThisTimeLineID = BootstrapTimeLineID;
 	checkPoint.PrevTimeLineID = BootstrapTimeLineID;
@@ -8252,6 +8253,7 @@ XLogReportParameters(void)
 			xl_parameter_change xlrec;
 			XLogRecPtr	recptr;
 
+			memset(&xlrec, 0, sizeof(xlrec));
 			xlrec.MaxConnections = MaxConnections;
 			xlrec.max_worker_processes = max_worker_processes;
 			xlrec.max_wal_senders = max_wal_senders;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd6d720..9b5ab9c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -2318,6 +2318,8 @@ ExecuteTruncateGuts(List *explicit_rels,
 		xl_heap_truncate xlrec;
 		int			i = 0;
 
+		memset(&xlrec, 0, sizeof(xlrec));
+
 		/* should only get here if effective_wal_level is 'logical' */
 		Assert(XLogLogicalInfoActive());
 
diff --git a/src/backend/replication/logical/message.c b/src/backend/replication/logical/message.c
index 06825d6..64a41c3 100644
--- a/src/backend/replication/logical/message.c
+++ b/src/backend/replication/logical/message.c
@@ -46,6 +46,8 @@ LogLogicalMessage(const char *prefix, const char *message, size_t size,
 	xl_logical_message xlrec;
 	XLogRecPtr	lsn;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	/*
 	 * Force xid to be allocated if we're emitting a transactional message.
 	 */
diff --git a/src/backend/replication/logical/origin.c b/src/backend/replication/logical/origin.c
index 26afd8f..df971f5 100644
--- a/src/backend/replication/logical/origin.c
+++ b/src/backend/replication/logical/origin.c
@@ -1011,6 +1011,7 @@ replorigin_advance(ReplOriginId node,
 	{
 		xl_replorigin_set xlrec;
 
+		memset(&xlrec, 0, sizeof(xlrec));
 		xlrec.remote_lsn = remote_commit;
 		xlrec.node_id = node;
 		xlrec.force = go_backward;
diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c
index f3ad90c..4ae5be2 100644
--- a/src/backend/storage/ipc/standby.c
+++ b/src/backend/storage/ipc/standby.c
@@ -1357,6 +1357,8 @@ LogCurrentRunningXacts(RunningTransactions CurrRunningXacts)
 	xl_running_xacts xlrec;
 	XLogRecPtr	recptr;
 
+	memset(&xlrec, 0, sizeof(xlrec));
+
 	xlrec.xcnt = CurrRunningXacts->xcnt;
 	xlrec.subxcnt = CurrRunningXacts->subxcnt;
 	xlrec.subxid_overflow = (CurrRunningXacts->subxid_status != SUBXIDS_IN_ARRAY);
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b..25d329e 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -695,6 +695,7 @@ AtPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state)
 		PgStat_TableStatus *tabstat PG_USED_FOR_ASSERTS_ONLY;
 		TwoPhasePgStatRecord record;
 
+		memset(&record, 0, sizeof(record));
 		Assert(trans->nest_level == 1);
 		Assert(trans->upper == NULL);
 		tabstat = trans->parent;
diff --git a/src/tools/valgrind.supp b/src/tools/valgrind.supp
index d56794b..f2ab0c8 100644
--- a/src/tools/valgrind.supp
+++ b/src/tools/valgrind.supp
@@ -23,23 +23,6 @@
 	fun:pgstat_write_statsfiles
 }
 
-{
-	padding_XLogRecData_CRC
-	Memcheck:Value8
-
-	fun:pg_comp_crc32c*
-	fun:XLogRecordAssemble
-}
-
-{
-	padding_XLogRecData_write
-	Memcheck:Param
-	pwrite64(buf)
-
-	...
-	fun:XLogWrite
-}
-
 {
 	padding_relcache
 	Memcheck:Param
diff --git a/src/test/regress/expected/hash_index.out b/src/test/regress/expected/hash_index.out
index 0403540..258d501 100644
--- a/src/test/regress/expected/hash_index.out
+++ b/src/test/regress/expected/hash_index.out
@@ -333,6 +333,25 @@ ROLLBACK;
 INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 50) as i;
 CHECKPOINT;
 VACUUM hash_cleanup_heap;
+-- Test insert-driven cleanup of dead index tuples (_hash_vacuum_one_page).
+TRUNCATE hash_cleanup_heap;
+INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 1000) as i;
+DELETE FROM hash_cleanup_heap
+  WHERE ctid IN ('(0,5)','(0,10)','(0,15)','(0,20)','(0,25)',
+                 '(0,30)','(0,35)','(0,40)','(0,45)','(0,50)');
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+SELECT count(*) FROM hash_cleanup_heap WHERE keycol = 1;
+ count 
+-------
+   990
+(1 row)
+
+INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 200) as i;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
 -- Clean up.
 DROP TABLE hash_cleanup_heap;
 -- Index on temp table.
diff --git a/src/test/regress/sql/hash_index.sql b/src/test/regress/sql/hash_index.sql
index 60571f6..1c85d98 100644
--- a/src/test/regress/sql/hash_index.sql
+++ b/src/test/regress/sql/hash_index.sql
@@ -314,6 +314,21 @@ INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 50) as i;
 CHECKPOINT;
 VACUUM hash_cleanup_heap;
 
+-- Test insert-driven cleanup of dead index tuples (_hash_vacuum_one_page).
+TRUNCATE hash_cleanup_heap;
+INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 1000) as i;
+DELETE FROM hash_cleanup_heap
+  WHERE ctid IN ('(0,5)','(0,10)','(0,15)','(0,20)','(0,25)',
+                 '(0,30)','(0,35)','(0,40)','(0,45)','(0,50)');
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+SELECT count(*) FROM hash_cleanup_heap WHERE keycol = 1;
+INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 200) as i;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
 -- Clean up.
 DROP TABLE hash_cleanup_heap;

---

From: Bertrand Drouvot <[email-masked]>
Date: Tue, 17 Mar 2026 12:43:18 +0000

Hi,

On Mon, Mar 16, 2026 at 05:14:10PM +0100, Alexander Kuzmenkov wrote:
> installcheck under Valgrind passes.

Thanks for the patch!

Without the memset part of the patch, I got valgrind's things like:

    214 heap_multi_insert (heapam.c:2425)
    149 heap_inplace_update_and_unlock (heapam.c:6592)
      5 palloc (mcxt.c:1411)
      3 _bt_getroot (nbtpage.c:348)
      2 log_heap_prune_and_freeze (pruneheap.c:2171)
      2 LogCurrentRunningXacts (standby.c:1356)
      1 vacuumRedirectAndPlaceholder (spgvacuum.c:495)
      1 _bt_set_cleanup_info (nbtpage.c:234)
      1 ??? (in /usr/lib/x86_64-linux-gnu/libcrypto.so.3)

and none with the patch applied. So the proposed changes look good to me.

One comment regarding the new memset(s) in the patch, I wonder if we should:

1/ Add a comment on top of them explaining why we are doing this and why
we don't use {0} (cf. Andres's point about C23 up-thread)

or

2/ Create a new macro, say INITIALIZE_PADDING or such with the comment on
top of it. That way, we could do things like:

+	INITIALIZE_PADDING(xlrec);
instead of
+	memset(&xlrec, 0, sizeof(xlrec));

I think that it would make the intent more clear and we could switch to {0} in a 
single place (if we feel the need) once C23 is required.

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

---

From: Zsolt Parragi <[email-masked]>
Date: Tue, 17 Mar 2026 18:45:46 +0000

Hello!

I'd like to propose a different approach: instead of relying on
valgrind and runtime detection of the issue, why don't we (also) add
specific static analysis rules to detect the situation at compile
time?

There are several threads when I had the same idea: maybe I should
write a postgres specific clang-tidy checker, and ask what everyone
thinks about integrating that into the build process in an optional
way?

I attached a WIP patch that addresses this, specifically for the xlog
padding problem for now.

src/tools/pg-tidy contains a basic custom clang-tidy plugin that works
based on two annotations (and helper macros that resolve to "" for
normal compilation):

* PG_NOPADDING can be used to mark that a struct doesn't have any
padding. If this annotation is added to a struct, but it has padding,
it will generate clang-tidy warnings. This is basically "-Wpadded",
but specifically for selected types.
* A separate check rule requires all PG_NOPADDING structs to be always
zero-initialized, meaning we don't have to rely on memset at all
* PG_REQUIRE_NOPADDING can be used to mark function arguments. If an
argument is marked with this, then the underlying type of its
parameter has to be either a primitive type, or a struct annotated
with PG_NOPADDING

Possible alternatives:
* I could simplify this by removing PG_NOPADDING, and instead checking
the requirement at every call site of a function with
PG_REQUIRE_NOPADDING. That would also mean that it could only enforce
zero-initialization when it's clearly visible in the same function. I
choose the two annotation approach for increased reliability
* I could simplify this to only check for end padding, enforce memset
instead of zero initialization, and build upon Alexander's previous
patch.

0001 implements and integrates pg-tidy (I only added it to the meson
build, if there's interest I can also add it to make. clang-tidy
integration works similarly to the llvm bitcode patch, so it is
properly parallelized/incremental)
0002 adds basic helper macros
0003 marks the data in XLogRegisterData with PG_REQUIRE_NOPADDING, all
related structs with PG_NOPADDING, and then fixes the padding issues
by adding explicit padding data instead of the compiler autogenerated
padding. We also cast a few arrays to char* (or alternatively we could
use nolint to suppress the check), because I didn't want to everywhere
zero initialize types like RelFileLocator.
0004 removes the now trivial SizeOf macros

Compared to only using valgrind, clang-tidy:
* works at compile time, guaranteed for every type used with xlog
* in theory should work with extensions (if we want to, e.g. by
integrating it into pgxs), without requiring extension developers to
add proper test workflows using valgrind
* valgrind should still work

What do you think? I'm interested in opinions about both the specific
case, and the generic idea of using custom clang-tidy checks for
various postgres-specific checks. As I mentioned at the beginning of
the message I think this could be useful for other things and doesn't
always require custom annotations, in several cases it could work
without any C code change.

---

From: Michael Paquier <[email-masked]>
Date: Wed, 18 Mar 2026 11:20:23 +0900

> without any C code change.

That's an interesting idea to be more aggressive in terms of the
checks done, but the invasiveness and the footprint this involves in
the WAL insertion code paths makes it a no-go for me.

Valgrind has proved to be quite useful over the years.  Sure, it takes
more time to run it, but for this specific issue I don't see why we
should not continue relying on it, not reinventing the wheel, and it's
served us pretty well.  While removing padding is a nice practice on
clean ground to make WAL records reproducible, that would mean forcing
the rule even for custom WAL RMGRs.  Some could say that they're OK to
live with some padding, and that we don't have to be strictly
aggressive at the code level.

Different opinions are of course welcome, that's just my feeling on
the matter about your proposal.
--
Michael

---

From: Zsolt Parragi <[email-masked]>
Date: Wed, 18 Mar 2026 06:33:21 +0000

Thank you for the feedback!

>  but the invasiveness and the footprint this involves in

Invasiveness is an option I choose, not a requirement.

In an alternative version, this could work in a "less strict" mode, on
top of Alexander's memset patch, verifying that: if we see a function
that uses XLogRegisterData, and the variable passed to it is defined
in the same function/translation unit (which is most of the case), we
require that variable to be well initialized - either all fields have
to be specified by hand, or it needs an initializer block/memset at
the beginning -- or if it has compiler generated padding inside, it
requires memset, as that's the only thing guaranteed to initialize it.
Similarly instead of requiring explicit padding added to the end of
the struct, it could instead verify that 1. the SizeOf macros are
correctly defined, refer to the proper size 2. if a SizeOf macro is
defined, the struct is properly memset at every location where it it
used

In that version, there would be little or no change over Alexander's
previous patch, other than adding pg-tidy itself to the build. I can
also create a version with that approach, it should be relatively
simply as I won't have to modify the WAL structs/calls like in this
version.

> should not continue relying on it

I'm not saying that we should rely on valgrind, it is a good tool and
it possibly catches things this wouldn't. This would be an additional
tool, offering the advantage of being quick and integrated into the
build. (Valgrind is also integrated, but it is also slow, I don't
think everyone runs it regularly as part of normal development)

---

From: Michael Paquier <[email-masked]>
Date: Wed, 18 Mar 2026 15:59:00 +0900

> +  WHERE ctid IN ('(0,5)','(0,10)','(0,15)','(0,20)','(0,25)',

Hmm.  If I take this SQL sequence independently or with an
installcheck, the one-page VACUUM path is taken during the final
INSERT, but that's not the case of a `make check`.  Could this be made
more stable?  I have not spent a lot of time on it, so I may be
missing something obvious, of course.
--
Michael

---

From: Alexander Kuzmenkov <[email-masked]>
Date: Wed, 18 Mar 2026 11:42:53 +0100

> missing something obvious, of course.

I think this might be caused by "make check" running many tests in
parallel, so the deleting transaction is visible to some snapshots, and the
cleanup is not done. Not sure what's the best way to improve this.

---

From: Heikki Linnakangas <[email-masked]>
Date: Wed, 18 Mar 2026 14:18:45 +0200

On 18/03/2026 12:42, Alexander Kuzmenkov wrote:
> On Wed, Mar 18, 2026 at 7:59 AM Michael Paquier <[email-masked] 
> <mailto:[email-masked]>> wrote:
> 
> parallel, so the deleting transaction is visible to some snapshots, and 
> the cleanup is not done. Not sure what's the best way to improve this.

I think if you use "BEGIN; INSERT ...; ROLLBACK;" to generate the dead 
tuples instead of DELETE, it will not be sensitive to concurrent 
snapshots like that.

- Heikki

---

From: Michael Paquier <[email-masked]>
Date: Sun, 22 Mar 2026 15:50:06 +0900

On Wed, Mar 18, 2026 at 02:18:45PM +0200, Heikki Linnakangas wrote:
> tuples instead of DELETE, it will not be sensitive to concurrent snapshots
> like that.

Nice trick, this makes the test stable.  I have reused it and applied
this part to take care of the coverage hole.
--
Michael