From 13450d14a1e064dd958b516ffa36b7a24bb49d7b Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Mon, 9 Jan 2023 10:23:10 -0800
Subject: [PATCH v2 1/3] Fix corruption due to vacuum_defer_cleanup_age
 underflowing 64bit xids

Author:
Reviewed-by:
Discussion: https://postgr.es/m/20230108002923.cyoser3ttmt63bfn@awork3.anarazel.de
Backpatch:
---
 src/backend/storage/ipc/procarray.c | 73 ++++++++++++++++++++++++-----
 1 file changed, 61 insertions(+), 12 deletions(-)

diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c
index 4340bf96416..d7d18ba8c12 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -367,6 +367,7 @@ static inline void ProcArrayEndTransactionInternal(PGPROC *proc, TransactionId l
 static void ProcArrayGroupClearXid(PGPROC *proc, TransactionId latestXid);
 static void MaintainLatestCompletedXid(TransactionId latestXid);
 static void MaintainLatestCompletedXidRecovery(TransactionId latestXid);
+static int ClampVacuumDeferCleanupAge(FullTransactionId rel, TransactionId xid);
 
 static inline FullTransactionId FullXidRelativeTo(FullTransactionId rel,
 												  TransactionId xid);
@@ -1888,17 +1889,32 @@ ComputeXidHorizons(ComputeXidHorizonsResult *h)
 		 * so guc.c should limit it to no more than the xidStopLimit threshold
 		 * in varsup.c.  Also note that we intentionally don't apply
 		 * vacuum_defer_cleanup_age on standby servers.
+		 *
+		 * Be careful to clamp vacuum_defer_cleanup age to prevent it from
+		 * creating an xid before FirstNormalTransactionId.
 		 */
-		h->oldest_considered_running =
-			TransactionIdRetreatedBy(h->oldest_considered_running,
-									 vacuum_defer_cleanup_age);
-		h->shared_oldest_nonremovable =
-			TransactionIdRetreatedBy(h->shared_oldest_nonremovable,
-									 vacuum_defer_cleanup_age);
-		h->data_oldest_nonremovable =
-			TransactionIdRetreatedBy(h->data_oldest_nonremovable,
-									 vacuum_defer_cleanup_age);
-		/* defer doesn't apply to temp relations */
+		Assert(TransactionIdPrecedesOrEquals(h->oldest_considered_running,
+											 h->shared_oldest_nonremovable));
+		Assert(TransactionIdPrecedesOrEquals(h->shared_oldest_nonremovable,
+											 h->data_oldest_nonremovable));
+
+		if (vacuum_defer_cleanup_age > 0)
+		{
+			int clamped_cleanup_age =
+				ClampVacuumDeferCleanupAge(h->latest_completed,
+										   h->oldest_considered_running);
+
+			h->oldest_considered_running =
+				TransactionIdRetreatedBy(h->oldest_considered_running,
+										 clamped_cleanup_age);
+			h->shared_oldest_nonremovable =
+				TransactionIdRetreatedBy(h->shared_oldest_nonremovable,
+										 clamped_cleanup_age);
+			h->data_oldest_nonremovable =
+				TransactionIdRetreatedBy(h->data_oldest_nonremovable,
+										 clamped_cleanup_age);
+			/* defer doesn't apply to temp relations */
+		}
 	}
 
 	/*
@@ -2469,9 +2485,17 @@ GetSnapshotData(Snapshot snapshot)
 		 */
 		oldestfxid = FullXidRelativeTo(latest_completed, oldestxid);
 
+		def_vis_xid_data = xmin;
+
 		/* apply vacuum_defer_cleanup_age */
-		def_vis_xid_data =
-			TransactionIdRetreatedBy(xmin, vacuum_defer_cleanup_age);
+		if (vacuum_defer_cleanup_age > 0)
+		{
+			int clamped_cleanup_age =
+				ClampVacuumDeferCleanupAge(oldestfxid, oldestxid);
+
+			def_vis_xid_data =
+				TransactionIdRetreatedBy(xmin, clamped_cleanup_age);
+		}
 
 		/* Check whether there's a replication slot requiring an older xmin. */
 		def_vis_xid_data =
@@ -4295,6 +4319,31 @@ GlobalVisCheckRemovableXid(Relation rel, TransactionId xid)
 	return GlobalVisTestIsRemovableXid(state, xid);
 }
 
+/*
+ * Clamp vacuum_defer_cleanup_age, to prevent it from retreating below
+ * FirstNormalTransactionId during epoch 0. This is important to prevent
+ * generating xids that cannot be converted to a FullTransactionId without
+ * wrapping around.
+ */
+static int
+ClampVacuumDeferCleanupAge(FullTransactionId rel, TransactionId oldest_xid)
+{
+	FullTransactionId oldest_fxid;
+	uint64 oldest_fxid_i;
+
+	if (vacuum_defer_cleanup_age == 0)
+		return 0;
+
+	oldest_fxid = FullXidRelativeTo(rel, oldest_xid);
+	oldest_fxid_i = U64FromFullTransactionId(oldest_fxid);
+
+	if (oldest_fxid_i < vacuum_defer_cleanup_age ||
+		(oldest_fxid_i - vacuum_defer_cleanup_age) < FirstNormalTransactionId)
+		return (int) (oldest_fxid_i - FirstNormalTransactionId);
+
+	return vacuum_defer_cleanup_age;
+}
+
 /*
  * Convert a 32 bit transaction id into 64 bit transaction id, by assuming it
  * is within MaxTransactionId / 2 of XidFromFullTransactionId(rel).
-- 
2.38.0

