Skip to content

Commit 3c92f7e

Browse files
committed
Fix corruption due to vacuum_defer_cleanup_age underflowing 64bit xids
When vacuum_defer_cleanup_age is bigger than the current xid, including the epoch, the subtraction of vacuum_defer_cleanup_age would lead to a wrapped around xid. While that normally is not a problem, the subsequent conversion to a 64bit xid results in a 64bit-xid very far into the future. As that xid is used as a horizon to detect whether rows versions are old enough to be removed, that allows removal of rows that are still visible (i.e. corruption). If vacuum_defer_cleanup_age was never changed from the default, there is no chance of this bug occurring. This bug was introduced in dc7420c. A lesser version of it exists in 12-13, introduced by fb5344c, affecting only GiST. The 12-13 version of the issue can, in rare cases, lead to pages in a gist index getting recycled too early, potentially causing index entries to be found multiple times. The fix is fairly simple - don't allow vacuum_defer_cleanup_age to retreat further than FirstNormalTransactionId. Patches to make similar bugs easier to find, by adding asserts to the 64bit xid infrastructure, have been proposed, but are not suitable for backpatching. Currently there are no tests for vacuum_defer_cleanup_age. A patch introducing infrastructure to make writing a test easier has been posted to the list. Reported-by: Michail Nikolaev <michail.nikolaev@gmail.com> Reviewed-by: Matthias van de Meent <boekewurm+postgres@gmail.com> Author: Andres Freund <andres@anarazel.de> Discussion: https://postgr.es/m/20230108002923.cyoser3ttmt63bfn@awork3.anarazel.de Backpatch: 12-, but impact/fix is smaller for 12-13
1 parent 5a19da5 commit 3c92f7e

File tree

1 file changed

+16
-4
lines changed

1 file changed

+16
-4
lines changed

src/backend/utils/time/snapmgr.c

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -983,24 +983,36 @@ GetFullRecentGlobalXmin(void)
983983
uint32 nextxid_epoch;
984984
TransactionId nextxid_xid;
985985
uint32 epoch;
986+
TransactionId horizon = RecentGlobalXmin;
986987

987-
Assert(TransactionIdIsNormal(RecentGlobalXmin));
988+
Assert(TransactionIdIsNormal(horizon));
988989

989990
/*
990991
* Compute the epoch from the next XID's epoch. This relies on the fact
991992
* that RecentGlobalXmin must be within the 2 billion XID horizon from the
992993
* next XID.
994+
*
995+
* Need to be careful to prevent wrapping around during epoch 0, otherwise
996+
* we would generate an xid far into the future when converting to a
997+
* FullTransactionId. This can happen because RecentGlobalXmin can be held
998+
* back via vacuum_defer_cleanup_age.
993999
*/
9941000
nextxid_full = ReadNextFullTransactionId();
9951001
nextxid_epoch = EpochFromFullTransactionId(nextxid_full);
9961002
nextxid_xid = XidFromFullTransactionId(nextxid_full);
9971003

998-
if (RecentGlobalXmin > nextxid_xid)
1004+
if (horizon <= nextxid_xid)
1005+
epoch = nextxid_epoch;
1006+
else if (nextxid_epoch > 0)
9991007
epoch = nextxid_epoch - 1;
10001008
else
1001-
epoch = nextxid_epoch;
1009+
{
1010+
/* don't wrap around */
1011+
epoch = 0;
1012+
horizon = FirstNormalTransactionId;
1013+
}
10021014

1003-
return FullTransactionIdFromEpochAndXid(epoch, RecentGlobalXmin);
1015+
return FullTransactionIdFromEpochAndXid(epoch, horizon);
10041016
}
10051017

10061018
/*

0 commit comments

Comments
 (0)