From 757483c3446dfd4566da32079d7ed45cf73ee0bc Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Fri, 7 Nov 2025 17:06:07 +0200
Subject: [PATCH 2/2] Fix truncation of multixid SLRUs at wraparound

SetOffsetVacuumLimit() and TruncateMultiXact() have checks for
MultiXactState->nextMXact == MultiXactState->oldestMultiXactId.
However, those checks didn't work as intended at wraparound. When the
last multixid before wraparound (UINT32_MAX) is consumed,
MultiXactState->nextMXact is advanced to 0, but because 0 is not a
valid multixid, all code that reads MultiXactState->nextMXact treats 0
as if the value was 1. Except for the checks in SetOffsetVacuumLimit()
and TruncateMultiXact().

As a result, at exactly multixid wraparound, VACUUM would fail to
truncate multixact SLRUs, or worse, it might truncate the offsets SLRU
incorrectly. I think the incorrect truncation is possible if a new
multixid is assigned concurrently just as vacuum reads the offsets
SLRU. The failure to truncate is easier to reproduce, but less
serious.

Discussion: XXX
---
 src/backend/access/transam/multixact.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 9d5f130af7e..735486f9df7 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -2673,6 +2673,9 @@ SetOffsetVacuumLimit(bool is_startup)
 	Assert(MultiXactState->finishedStartup);
 	LWLockRelease(MultiXactGenLock);
 
+	if (nextMXact < FirstMultiXactId)
+		nextMXact = FirstMultiXactId;
+
 	/*
 	 * Determine the offset of the oldest multixact.  Normally, we can read
 	 * the offset from the multixact itself, but there's an important special
@@ -3075,6 +3078,9 @@ TruncateMultiXact(MultiXactId newOldestMulti, Oid newOldestMultiDB)
 	LWLockRelease(MultiXactGenLock);
 	Assert(MultiXactIdIsValid(oldestMulti));
 
+	if (nextMulti < FirstMultiXactId)
+		nextMulti = FirstMultiXactId;
+
 	/*
 	 * Make sure to only attempt truncation if there's values to truncate
 	 * away. In normal processing values shouldn't go backwards, but there's
-- 
2.47.3

