From ea7af58c794321887f3492cc1b741b3614d15180 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Thu, 17 Sep 2026 18:13:54 +0300
Subject: [PATCH 1/1] Give a special error if multixid 1 is missing on
 pg_upgrade

---
 src/bin/pg_upgrade/multixact_read_v18.c | 21 ++++++-
 src/bin/pg_upgrade/multixact_read_v18.h |  2 +-
 src/bin/pg_upgrade/multixact_rewrite.c  |  6 +-
 src/bin/pg_upgrade/slru_io.c            | 74 +++++++++++++++++++------
 src/bin/pg_upgrade/slru_io.h            |  1 +
 5 files changed, 82 insertions(+), 22 deletions(-)

diff --git a/src/bin/pg_upgrade/multixact_read_v18.c b/src/bin/pg_upgrade/multixact_read_v18.c
index c92b977530a..08fce9e7186 100644
--- a/src/bin/pg_upgrade/multixact_read_v18.c
+++ b/src/bin/pg_upgrade/multixact_read_v18.c
@@ -116,7 +116,8 @@ MXOffsetToFlagsBitShift(MultiXactOffset32 offset)
  * Returns the malloced memory used by the all other calls in this module.
  */
 OldMultiXactReader *
-AllocOldMultiXactRead(char *pgdata, MultiXactId nextMulti,
+AllocOldMultiXactRead(char *pgdata,
+					  MultiXactId oldestMulti, MultiXactId nextMulti,
 					  MultiXactOffset32 nextOffset)
 {
 	OldMultiXactReader *state = pg_malloc_object(OldMultiXactReader);
@@ -131,6 +132,24 @@ AllocOldMultiXactRead(char *pgdata, MultiXactId nextMulti,
 	pg_sprintf(dir, "%s/pg_multixact/members", pgdata);
 	state->members = AllocSlruRead(dir, false);
 
+	/*
+	 * If oldestMulti is 1 (FirstMultiXactId), check that the corresponding
+	 * offsets segment exists.  You'd get an error later anyway when trying to
+	 * read it, but we want to give a special error message for that case
+	 * because there was a bug in old versions of pg_upgrade where oldestMulti
+	 * was incorrectly set to 1.  See commit a61daa14d5 that fixed that bug.
+	 */
+	if (oldestMulti == FirstMultiXactId && nextMulti != oldestMulti &&
+		!SlruReadSegmentExists(state->offset, MultiXactIdToOffsetPage(oldestMulti)))
+	{
+		pg_log(PG_REPORT, "fatal");
+		pg_fatal("Segment file containing multixid %u does not exist.\n"
+				 "This can happen if an old version of pg_upgrade was used in the\n"
+				 "past to upgrade the cluster to version 9.3. If that is the cause,\n"
+				 "it can be fixed by running VACUUM FREEZE before upgrade.",
+				 oldestMulti);
+	}
+
 	return state;
 }
 
diff --git a/src/bin/pg_upgrade/multixact_read_v18.h b/src/bin/pg_upgrade/multixact_read_v18.h
index 88b52b9d2e8..bc4b73d452c 100644
--- a/src/bin/pg_upgrade/multixact_read_v18.h
+++ b/src/bin/pg_upgrade/multixact_read_v18.h
@@ -27,7 +27,7 @@ typedef struct OldMultiXactReader
 } OldMultiXactReader;
 
 extern OldMultiXactReader *AllocOldMultiXactRead(char *pgdata,
-												 MultiXactId nextMulti,
+												 MultiXactId oldestMulti, MultiXactId nextMulti,
 												 MultiXactOffset32 nextOffset);
 extern bool GetOldMultiXactIdSingleMember(OldMultiXactReader *state,
 										  MultiXactId multi,
diff --git a/src/bin/pg_upgrade/multixact_rewrite.c b/src/bin/pg_upgrade/multixact_rewrite.c
index c7a1416494d..c670ff36fe0 100644
--- a/src/bin/pg_upgrade/multixact_rewrite.c
+++ b/src/bin/pg_upgrade/multixact_rewrite.c
@@ -62,7 +62,7 @@ rewrite_multixacts(MultiXactId from_multi, MultiXactId to_multi)
 	 * old cluster.
 	 */
 	old_reader = AllocOldMultiXactRead(old_cluster.pgdata,
-									   old_cluster.controldata.chkpnt_nxtmulti,
+									   from_multi, old_cluster.controldata.chkpnt_nxtmulti,
 									   old_cluster.controldata.chkpnt_nxtmxoff);
 
 	for (MultiXactId multi = from_multi; multi != to_multi;)
@@ -76,8 +76,8 @@ rewrite_multixacts(MultiXactId from_multi, MultiXactId to_multi)
 		 * Locking-only XIDs that may be part of multi-xids don't matter after
 		 * upgrade, as there can be no transactions running across upgrade. So
 		 * as a small optimization, we only read one member from each
-		 * multixid: the one updating one, or if there was no update,
-		 * arbitrarily the first locking xid.
+		 * multixid: the updating one, or if there was no update, arbitrarily
+		 * the first locking xid.
 		 */
 		multixid_valid = GetOldMultiXactIdSingleMember(old_reader, multi, &member);
 
diff --git a/src/bin/pg_upgrade/slru_io.c b/src/bin/pg_upgrade/slru_io.c
index aa9d59a0d7b..caa1a1859b2 100644
--- a/src/bin/pg_upgrade/slru_io.c
+++ b/src/bin/pg_upgrade/slru_io.c
@@ -20,6 +20,8 @@
 
 static SlruSegState *AllocSlruSegState(const char *dir);
 static char *SlruFileName(SlruSegState *state, int64 segno);
+static bool SlruReadSwitchSegment(SlruSegState *state, int64 segno, bool missing_ok);
+static void SlruReadCloseSegment(SlruSegState *state);
 static void SlruFlush(SlruSegState *state);
 
 /* common parts of AllocSlruRead and AllocSlruWrite */
@@ -69,6 +71,49 @@ AllocSlruRead(const char *dir, bool long_segment_names)
 	return state;
 }
 
+/*
+ * Open the given segment, closing old one first if required.
+ *
+ * If 'missing_ok' is true and the file does not exist, returns false.
+ * Otherwise a missing file is fatal.
+ */
+static bool
+SlruReadSwitchSegment(SlruSegState *state, int64 segno, bool missing_ok)
+{
+	SlruReadCloseSegment(state);
+
+	state->fn = SlruFileName(state, segno);
+	if ((state->fd = open(state->fn, O_RDONLY | PG_BINARY, 0)) < 0)
+	{
+		if (missing_ok && errno == ENOENT)
+		{
+			pg_free(state->fn);
+			return false;
+		}
+		pg_fatal("could not open file \"%s\": %m", state->fn);
+	}
+	state->segno = segno;
+	return true;
+}
+
+/*
+ * Close the current segment file, if any.
+ */
+static void
+SlruReadCloseSegment(SlruSegState *state)
+{
+	if (state->segno != -1)
+	{
+		close(state->fd);
+		state->fd = -1;
+
+		pg_free(state->fn);
+		state->fn = NULL;
+
+		state->segno = -1;
+	}
+}
+
 /*
  * Read the given page into memory buffer.
  *
@@ -96,23 +141,7 @@ SlruReadSwitchPageSlow(SlruSegState *state, uint64 pageno)
 	/* If the new page is on a different SLRU segment, open the new segment */
 	segno = pageno / SLRU_PAGES_PER_SEGMENT;
 	if (segno != state->segno)
-	{
-		if (state->segno != -1)
-		{
-			close(state->fd);
-			state->fd = -1;
-
-			pg_free(state->fn);
-			state->fn = NULL;
-
-			state->segno = -1;
-		}
-
-		state->fn = SlruFileName(state, segno);
-		if ((state->fd = open(state->fn, O_RDONLY | PG_BINARY, 0)) < 0)
-			pg_fatal("could not open file \"%s\": %m", state->fn);
-		state->segno = segno;
-	}
+		SlruReadSwitchSegment(state, segno, false);
 
 	offset = (pageno % SLRU_PAGES_PER_SEGMENT) * BLCKSZ;
 	bytes_read = 0;
@@ -146,6 +175,17 @@ SlruReadSwitchPageSlow(SlruSegState *state, uint64 pageno)
 	return state->buf.data;
 }
 
+/*
+ * Return true if the segment file containing given page exists.
+ */
+bool
+SlruReadSegmentExists(SlruSegState *state, uint64 pageno)
+{
+	int64		segno = pageno / SLRU_PAGES_PER_SEGMENT;
+
+	return SlruReadSwitchSegment(state, segno, true);
+}
+
 /*
  * Free the reader.
  */
diff --git a/src/bin/pg_upgrade/slru_io.h b/src/bin/pg_upgrade/slru_io.h
index 0be83b28615..de871e37641 100644
--- a/src/bin/pg_upgrade/slru_io.h
+++ b/src/bin/pg_upgrade/slru_io.h
@@ -26,6 +26,7 @@ typedef struct SlruSegState
 } SlruSegState;
 
 extern SlruSegState *AllocSlruRead(const char *dir, bool long_segment_names);
+extern bool SlruReadSegmentExists(SlruSegState *state, uint64 pageno);
 extern char *SlruReadSwitchPageSlow(SlruSegState *state, uint64 pageno);
 extern void FreeSlruRead(SlruSegState *state);
 
-- 
2.47.3

