Good day.

multixact.c has bug in initialization and access of OldestMemberMXactId
(and partially OldestVisibleMXactId).

Size of this arrays is defined as:

 #define MaxOldestSlot (MaxBackends + max_prepared_xacts)

assuming there are only backends and prepared transactions could hold
multixacts.

This assumption is correct. And in fact there were no bug when this formula
were introduced in 2009y [1], since these arrays were indexed but synthetic
dummy `dummyBackendId` field of `GlobalTransactionData` struct.

But in 2024y [2] field `dummyBackendId` were removed and pgprocno were used
instead.

But proc structs reserved for two phase commit are placed after auxiliary
procs, therefore writes to OldestMemberMXactId[dummy] starts to overwrites
slots of OldestVisibleMXactId.

Then PostgreSQL 18 increased NUM_AUXILIARY_PROCS due to reserve of workers
for AIO. And it is possible to make such test postgresql.conf with so
extremely low MaxBackend so writes to OldestMemberMXactId[dummy] overwrites
first entry of BufferDescriptors, which are allocated next in shared memory.

Patch in attach replaces direct accesses to this arrays with inline
functions which include asserts. And changes calculation of MaxOldestSlot
to include NUM_AUXILIARY_PROCS.

Certainly, it is not clearest patch possible:
- may be you will decide to not introduce inline functions,
- or will introduce separate inline function for each array,
- or will fix slot calculation to remove aux procs from account,
- or will revert deletion of dummyBackendId.

[1]
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=cd87b6f8a5084c070c3e56b07794be8fea33647d
[2]
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=ab355e3a88de745607f6dd4c21f0119b5c68f2ad

-- 
regards
Yura Sokolov aka funny-falcon
From f518e1454d5ee09eee1a84ab43bec1150361e76b Mon Sep 17 00:00:00 2001
From: Yura Sokolov <[email protected]>
Date: Tue, 24 Feb 2026 20:29:01 +0300
Subject: [PATCH v00] Fix multixacts OldestMemberMXactId and
 OldestVisibleMXactId initialization.

Due to [1], OldestMemberMXactId is no longer accessed by synthetic
dummyBackendId, but rather with pgprocno. Procs for prepared xacts are
placed after auxiliary procs, therefore calculation for MaxOldestSlot
became invalid.

Fix MaxOldestSlot to include NUM_AUXILIARY_PROCS and replace direct access
to these arrays with inline functions.

[1] ab355e3a88de745 "Redefine backend ID to be an index into the proc array"
---
 src/backend/access/transam/multixact.c | 58 ++++++++++++++++++--------
 1 file changed, 41 insertions(+), 17 deletions(-)

diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 90ec87d9dd6..15dd928eb95 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -210,7 +210,7 @@ typedef struct MultiXactStateData
 /*
  * Size of OldestMemberMXactId and OldestVisibleMXactId arrays.
  */
-#define MaxOldestSlot	(MaxBackends + max_prepared_xacts)
+#define MaxOldestSlot	(MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts)
 
 /* Pointers to the state data in shared memory */
 static MultiXactStateData *MultiXactState;
@@ -285,6 +285,9 @@ static void WriteMTruncateXlogRec(Oid oldestMultiDB,
 								  MultiXactId endTruncOff,
 								  MultiXactOffset endTruncMemb);
 
+static inline MultiXactId getOldest(MultiXactId *oldest, ProcNumber procno);
+static inline bool validOldest(MultiXactId *oldest, ProcNumber procno);
+static inline void setOldest(MultiXactId *oldest, ProcNumber procno, MultiXactId mxact);
 
 /*
  * MultiXactIdCreate
@@ -308,7 +311,7 @@ MultiXactIdCreate(TransactionId xid1, MultiXactStatus status1,
 	Assert(!TransactionIdEquals(xid1, xid2) || (status1 != status2));
 
 	/* MultiXactIdSetOldestMember() must have been called already. */
-	Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+	Assert(validOldest(OldestMemberMXactId, MyProcNumber));
 
 	/*
 	 * Note: unlike MultiXactIdExpand, we don't bother to check that both XIDs
@@ -362,7 +365,7 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status)
 	Assert(TransactionIdIsValid(xid));
 
 	/* MultiXactIdSetOldestMember() must have been called already. */
-	Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]));
+	Assert(validOldest(OldestMemberMXactId, MyProcNumber));
 
 	debug_elog5(DEBUG2, "Expand: received multi %u, xid %u status %s",
 				multi, xid, mxstatus_to_string(status));
@@ -536,7 +539,7 @@ MultiXactIdIsRunning(MultiXactId multi, bool isLockOnly)
 void
 MultiXactIdSetOldestMember(void)
 {
-	if (!MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber]))
+	if (!validOldest(OldestMemberMXactId, MyProcNumber))
 	{
 		MultiXactId nextMXact;
 
@@ -558,7 +561,7 @@ MultiXactIdSetOldestMember(void)
 
 		nextMXact = MultiXactState->nextMXact;
 
-		OldestMemberMXactId[MyProcNumber] = nextMXact;
+		setOldest(OldestMemberMXactId, MyProcNumber, nextMXact);
 
 		LWLockRelease(MultiXactGenLock);
 
@@ -586,7 +589,7 @@ MultiXactIdSetOldestMember(void)
 static void
 MultiXactIdSetOldestVisible(void)
 {
-	if (!MultiXactIdIsValid(OldestVisibleMXactId[MyProcNumber]))
+	if (!validOldest(OldestVisibleMXactId, MyProcNumber))
 	{
 		MultiXactId oldestMXact;
 		int			i;
@@ -603,7 +606,7 @@ MultiXactIdSetOldestVisible(void)
 				oldestMXact = thisoldest;
 		}
 
-		OldestVisibleMXactId[MyProcNumber] = oldestMXact;
+		setOldest(OldestVisibleMXactId, MyProcNumber, oldestMXact);
 
 		LWLockRelease(MultiXactGenLock);
 
@@ -1152,7 +1155,7 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members,
 	 * multi.  It cannot possibly still be running.
 	 */
 	if (isLockOnly &&
-		MultiXactIdPrecedes(multi, OldestVisibleMXactId[MyProcNumber]))
+		MultiXactIdPrecedes(multi, getOldest(OldestVisibleMXactId, MyProcNumber)))
 	{
 		debug_elog2(DEBUG2, "GetMembers: a locker-only multi is too old");
 		*members = NULL;
@@ -1574,8 +1577,8 @@ AtEOXact_MultiXact(void)
 	 * We assume that storing a MultiXactId is atomic and so we need not take
 	 * MultiXactGenLock to do this.
 	 */
-	OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
-	OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+	setOldest(OldestMemberMXactId, MyProcNumber, InvalidMultiXactId);
+	setOldest(OldestVisibleMXactId, MyProcNumber, InvalidMultiXactId);
 
 	/*
 	 * Discard the local MultiXactId cache.  Since MXactContext was created as
@@ -1595,7 +1598,7 @@ AtEOXact_MultiXact(void)
 void
 AtPrepare_MultiXact(void)
 {
-	MultiXactId myOldestMember = OldestMemberMXactId[MyProcNumber];
+	MultiXactId myOldestMember = getOldest(OldestMemberMXactId, MyProcNumber);
 
 	if (MultiXactIdIsValid(myOldestMember))
 		RegisterTwoPhaseRecord(TWOPHASE_RM_MULTIXACT_ID, 0,
@@ -1615,7 +1618,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
 	 * Transfer our OldestMemberMXactId value to the slot reserved for the
 	 * prepared transaction.
 	 */
-	myOldestMember = OldestMemberMXactId[MyProcNumber];
+	myOldestMember = getOldest(OldestMemberMXactId, MyProcNumber);
 	if (MultiXactIdIsValid(myOldestMember))
 	{
 		ProcNumber	dummyProcNumber = TwoPhaseGetDummyProcNumber(fxid, false);
@@ -1628,8 +1631,8 @@ PostPrepare_MultiXact(FullTransactionId fxid)
 		 */
 		LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
 
-		OldestMemberMXactId[dummyProcNumber] = myOldestMember;
-		OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId;
+		setOldest(OldestMemberMXactId, dummyProcNumber, myOldestMember);
+		setOldest(OldestMemberMXactId, MyProcNumber, InvalidMultiXactId);
 
 		LWLockRelease(MultiXactGenLock);
 	}
@@ -1642,7 +1645,7 @@ PostPrepare_MultiXact(FullTransactionId fxid)
 	 * We assume that storing a MultiXactId is atomic and so we need not take
 	 * MultiXactGenLock to do this.
 	 */
-	OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId;
+	setOldest(OldestVisibleMXactId, MyProcNumber, InvalidMultiXactId);
 
 	/*
 	 * Discard the local MultiXactId cache like in AtEOXact_MultiXact.
@@ -1669,7 +1672,7 @@ multixact_twophase_recover(FullTransactionId fxid, uint16 info,
 	Assert(len == sizeof(MultiXactId));
 	oldestMember = *((MultiXactId *) recdata);
 
-	OldestMemberMXactId[dummyProcNumber] = oldestMember;
+	setOldest(OldestMemberMXactId, dummyProcNumber, oldestMember);
 }
 
 /*
@@ -1684,7 +1687,7 @@ multixact_twophase_postcommit(FullTransactionId fxid, uint16 info,
 
 	Assert(len == sizeof(MultiXactId));
 
-	OldestMemberMXactId[dummyProcNumber] = InvalidMultiXactId;
+	setOldest(OldestMemberMXactId, dummyProcNumber, InvalidMultiXactId);
 }
 
 /*
@@ -2922,3 +2925,24 @@ multixactmemberssyncfiletag(const FileTag *ftag, char *path)
 {
 	return SlruSyncFileTag(MultiXactMemberCtl, ftag, path);
 }
+
+static inline MultiXactId
+getOldest(MultiXactId *oldest, ProcNumber procno)
+{
+	Assert(procno < MaxOldestSlot);
+	return oldest[procno];
+}
+
+static inline bool
+validOldest(MultiXactId *oldest, ProcNumber procno)
+{
+	Assert(procno < MaxOldestSlot);
+	return MultiXactIdIsValid(oldest[procno]);
+}
+
+static inline void
+setOldest(MultiXactId *oldest, ProcNumber procno, MultiXactId mxact)
+{
+	Assert(procno < MaxOldestSlot);
+	oldest[procno] = mxact;
+}
-- 
2.51.0

Reply via email to