From 866999c1251fbbde2f1d9db2799734c7c6e8fe54 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Fri, 18 Sep 2026 20:57:26 +0300 Subject: [PATCH 1/1] Check that oldestXID and oldestMulti are consistent at pg_upgrade Now that pg_upgrade will rewrite multixid members, starting from oldestMulti, it's important that oldestMulti is valid. Add a sanity check that oldestMulti is not newer than the oldest datminmxid value in pg_database. One case where this could happen is if the cluster was previously upgraded to version 9.3 with a buggy pg_upgrade version that didn't have commit a61daa14d5. This new pg_upgrade check is similar to the defence that was added in commit 78db307bb2 to VACUUM to avoid truncating away multixids if oldestMulti is too new. This pg_upgrade check differs in that we don't try to soldier on with the upgrade if the oldestMultiXID is inconsistent, but rather just abort the upgrade. Reported-by: Noah Misch Discussion: https://www.postgresql.org/message-id/20260827231757.78.noahmisch@microsoft.com Backpatch-through: 19 --- src/backend/access/transam/multixact.c | 28 ----------- src/bin/pg_upgrade/check.c | 68 ++++++++++++++++++++++++++ src/include/access/multixact.h | 32 ++++++++++-- 3 files changed, 97 insertions(+), 31 deletions(-) diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c index 70a4ea69486..3caedb54850 100644 --- a/src/backend/access/transam/multixact.c +++ b/src/backend/access/transam/multixact.c @@ -2860,34 +2860,6 @@ MultiXactMemberIoErrorDetail(const void *opaque_data) context->offset); } -/* - * Decide which of two MultiXactIds is earlier. - * - * XXX do we need to do something special for InvalidMultiXactId? - * (Doesn't look like it.) - */ -bool -MultiXactIdPrecedes(MultiXactId multi1, MultiXactId multi2) -{ - int32 diff = (int32) (multi1 - multi2); - - return (diff < 0); -} - -/* - * MultiXactIdPrecedesOrEquals -- is multi1 logically <= multi2? - * - * XXX do we need to do something special for InvalidMultiXactId? - * (Doesn't look like it.) - */ -bool -MultiXactIdPrecedesOrEquals(MultiXactId multi1, MultiXactId multi2) -{ - int32 diff = (int32) (multi1 - multi2); - - return (diff <= 0); -} - /* * Write a TRUNCATE xlog record diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c index 41342561763..758fb8fb62b 100644 --- a/src/bin/pg_upgrade/check.c +++ b/src/bin/pg_upgrade/check.c @@ -9,6 +9,8 @@ #include "postgres_fe.h" +#include "access/multixact.h" +#include "access/transam.h" #include "catalog/pg_am_d.h" #include "catalog/pg_authid_d.h" #include "catalog/pg_class_d.h" @@ -36,6 +38,7 @@ static void check_new_cluster_subscription_configuration(void); static void check_old_cluster_for_valid_slots(void); static void check_old_cluster_subscription_state(void); static void check_old_cluster_global_names(ClusterInfo *cluster); +static void check_for_oldestXid_consistency(ClusterInfo *cluster); /* * DataTypesUsageChecks - definitions of data type checks for the old cluster @@ -570,6 +573,7 @@ check_and_dump_old_cluster(void) */ check_is_install_user(&old_cluster); check_for_prepared_transactions(&old_cluster); + check_for_oldestXid_consistency(&old_cluster); check_for_isn_and_int8_passing_mismatch(&old_cluster); if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1700) @@ -2570,3 +2574,67 @@ check_old_cluster_global_names(ClusterInfo *cluster) else check_ok(); } + +/* + * check_for_oldestXid_consistency() + * + * Check that the oldestXID and oldestMultiXID values in the control file are + * consistent with the 'datfrozenxid' and 'datminmxid' values in pg_database. + * + * The invariant is that value in the control file must always be equal or + * older than the oldest datfrozenxid. Otherwise you might already have + * truncated away clog or multixids that are still needed. If that has + * happened, we refuse the upgrade and require the administrator to deal with + * the situation first. + * + * One scenario where that is known to happen is if the cluster was upgraded + * in the past to version 9.3 with a buggy pg_upgrade version that didn't copy + * the oldestMulti value from the old cluster. See commit a61daa14d5. That + * was a long time ago, though, so you're not very likely to encounter that + * bug in the wild anymore. Therefore we don't assume that's the cause or try + * to do anything clever here. In any case, it's still good to check to + * prevent further damage. + */ +static void +check_for_oldestXid_consistency(ClusterInfo *cluster) +{ + PGconn *conn_template1; + PGresult *dbres; + int ntups; + int i_datname; + int i_datfrozenxid; + int i_datminmxid; + + prep_status("Checking oldestXID and oldestMultiXid consistency"); + + conn_template1 = connectToServer(cluster, "template1"); + + dbres = executeQueryOrDie(conn_template1, + "SELECT datname, datfrozenxid, datminmxid " + "FROM pg_catalog.pg_database"); + + i_datname = PQfnumber(dbres, "datname"); + i_datfrozenxid = PQfnumber(dbres, "datfrozenxid"); + i_datminmxid = PQfnumber(dbres, "datminmxid"); + + ntups = PQntuples(dbres); + for (int dbnum = 0; dbnum < ntups; dbnum++) + { + char *datname = PQgetvalue(dbres, dbnum, i_datname); + TransactionId datfrozenxid = (TransactionId) str2uint(PQgetvalue(dbres, dbnum, i_datfrozenxid)); + MultiXactId datminmxid = (MultiXactId) str2uint(PQgetvalue(dbres, dbnum, i_datminmxid)); + + if (TransactionIdPrecedes(datfrozenxid, cluster->controldata.chkpnt_oldstxid)) + { + pg_fatal("oldestXID (%u) in the control file is newer than the datfrozenxid (%u) of database \"%s\"", + cluster->controldata.chkpnt_oldstxid, datfrozenxid, datname); + } + if (MultiXactIdPrecedes(datminmxid, cluster->controldata.chkpnt_oldstMulti)) + { + pg_fatal("oldestMultiXid (%u) in control file is newer than the datminmxid (%u) of database \"%s\"", + cluster->controldata.chkpnt_oldstMulti, datminmxid, datname); + } + } + + check_ok(); +} diff --git a/src/include/access/multixact.h b/src/include/access/multixact.h index 6be5299ab68..503ec327404 100644 --- a/src/include/access/multixact.h +++ b/src/include/access/multixact.h @@ -110,9 +110,35 @@ extern int GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members, extern void GetMultiXactInfo(uint32 *multixacts, MultiXactOffset *nextOffset, MultiXactId *oldestMultiXactId, MultiXactOffset *oldestOffset); -extern bool MultiXactIdPrecedes(MultiXactId multi1, MultiXactId multi2); -extern bool MultiXactIdPrecedesOrEquals(MultiXactId multi1, - MultiXactId multi2); + + +/* + * Decide which of two MultiXactIds is earlier. + * + * XXX do we need to do something special for InvalidMultiXactId? + * (Doesn't look like it.) + */ +static inline bool +MultiXactIdPrecedes(MultiXactId multi1, MultiXactId multi2) +{ + int32 diff = (int32) (multi1 - multi2); + + return (diff < 0); +} + +/* + * MultiXactIdPrecedesOrEquals -- is multi1 logically <= multi2? + * + * XXX do we need to do something special for InvalidMultiXactId? + * (Doesn't look like it.) + */ +static inline bool +MultiXactIdPrecedesOrEquals(MultiXactId multi1, MultiXactId multi2) +{ + int32 diff = (int32) (multi1 - multi2); + + return (diff <= 0); +} extern int multixactoffsetssyncfiletag(const FileTag *ftag, char *path); extern int multixactmemberssyncfiletag(const FileTag *ftag, char *path); -- 2.47.3