Fix bug in following update chain when locking a heap tuple REL_15_STABLE github/REL_15_STABLE
authorHeikki Linnakangas <[email protected]>
Tue, 23 Dec 2025 11:37:16 +0000 (13:37 +0200)
committerHeikki Linnakangas <[email protected]>
Tue, 23 Dec 2025 11:37:29 +0000 (13:37 +0200)
After waiting for a concurrent updater to finish, heap_lock_tuple()
followed the update chain to lock all tuple versions. However, when
stepping from the initial tuple to the next one, it failed to check
that the next tuple's XMIN matches the initial tuple's XMAX. That's an
important check whenever following an update chain, and the recursive
part that follows the chain did it, but the initial step missed it.
Without the check, if the updating transaction aborts, the updated
tuple is vacuumed away and replaced by an unrelated tuple, the
unrelated tuple might get incorrectly locked.

Author: Jasper Smit <[email protected]>
Discussion: https://www.postgresql.org/message-id/CAOG+RQ74x0q=kgBBQ=mezuvOeZBfSxM1qu_o0V28bwDz3dHxLw@mail.gmail.com
Backpatch-through: 14

src/backend/access/heap/heapam.c

index 64044de67b28be6822b41ad208a7f22b8c3b9302..ef0e5eeca308388ad3d8901de65a0b49b04c0a04 100644 (file)
@@ -101,8 +101,11 @@ static void compute_new_xmax_infomask(TransactionId xmax, uint16 old_infomask,
                                      LockTupleMode mode, bool is_update,
                                      TransactionId *result_xmax, uint16 *result_infomask,
                                      uint16 *result_infomask2);
-static TM_Result heap_lock_updated_tuple(Relation rel, HeapTuple tuple,
-                                        ItemPointer ctid, TransactionId xid,
+static TM_Result heap_lock_updated_tuple(Relation rel,
+                                        uint16 prior_infomask,
+                                        TransactionId prior_rawxmax,
+                                        const ItemPointerData *prior_ctid,
+                                        TransactionId xid,
                                         LockTupleMode mode);
 static void GetMultiXactIdHintBits(MultiXactId multi, uint16 *new_infomask,
                                   uint16 *new_infomask2);
@@ -4691,11 +4694,13 @@ l3:
                 * If there are updates, follow the update chain; bail out if
                 * that cannot be done.
                 */
-               if (follow_updates && updated)
+               if (follow_updates && updated &&
+                   !ItemPointerEquals(&tuple->t_self, &t_ctid))
                {
                    TM_Result   res;
 
-                   res = heap_lock_updated_tuple(relation, tuple, &t_ctid,
+                   res = heap_lock_updated_tuple(relation,
+                                                 infomask, xwait, &t_ctid,
                                                  GetCurrentTransactionId(),
                                                  mode);
                    if (res != TM_Ok)
@@ -4938,11 +4943,13 @@ l3:
            }
 
            /* if there are updates, follow the update chain */
-           if (follow_updates && !HEAP_XMAX_IS_LOCKED_ONLY(infomask))
+           if (follow_updates && !HEAP_XMAX_IS_LOCKED_ONLY(infomask) &&
+               !ItemPointerEquals(&tuple->t_self, &t_ctid))
            {
                TM_Result   res;
 
-               res = heap_lock_updated_tuple(relation, tuple, &t_ctid,
+               res = heap_lock_updated_tuple(relation,
+                                             infomask, xwait, &t_ctid,
                                              GetCurrentTransactionId(),
                                              mode);
                if (res != TM_Ok)
@@ -5596,7 +5603,8 @@ test_lockmode_for_conflict(MultiXactStatus status, TransactionId xid,
  * version as well.
  */
 static TM_Result
-heap_lock_updated_tuple_rec(Relation rel, ItemPointer tid, TransactionId xid,
+heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax,
+                           const ItemPointerData *tid, TransactionId xid,
                            LockTupleMode mode)
 {
    TM_Result   result;
@@ -5609,7 +5617,6 @@ heap_lock_updated_tuple_rec(Relation rel, ItemPointer tid, TransactionId xid,
                old_infomask2;
    TransactionId xmax,
                new_xmax;
-   TransactionId priorXmax = InvalidTransactionId;
    bool        cleared_all_frozen = false;
    bool        pinned_desired_page;
    Buffer      vmbuffer = InvalidBuffer;
@@ -5923,7 +5930,10 @@ out_unlocked:
  *     Follow update chain when locking an updated tuple, acquiring locks (row
  *     marks) on the updated versions.
  *
- * The initial tuple is assumed to be already locked.
+ * 'prior_infomask', 'prior_raw_xmax' and 'prior_ctid' are the corresponding
+ * fields from the initial tuple.  We will lock the tuples starting from the
+ * one that 'prior_ctid' points to.  Note: This function does not lock the
+ * initial tuple itself.
  *
  * This function doesn't check visibility, it just unconditionally marks the
  * tuple(s) as locked.  If any tuple in the updated chain is being deleted
@@ -5941,16 +5951,20 @@ out_unlocked:
  * levels, because that would lead to a serializability failure.
  */
 static TM_Result
-heap_lock_updated_tuple(Relation rel, HeapTuple tuple, ItemPointer ctid,
+heap_lock_updated_tuple(Relation rel,
+                       uint16 prior_infomask,
+                       TransactionId prior_raw_xmax,
+                       const ItemPointerData *prior_ctid,
                        TransactionId xid, LockTupleMode mode)
 {
    /*
-    * If the tuple has not been updated, or has moved into another partition
-    * (effectively a delete) stop here.
+    * If the tuple has moved into another partition (effectively a delete)
+    * stop here.
     */
-   if (!HeapTupleHeaderIndicatesMovedPartitions(tuple->t_data) &&
-       !ItemPointerEquals(&tuple->t_self, ctid))
+   if (!ItemPointerIndicatesMovedPartitions(prior_ctid))
    {
+       TransactionId prior_xmax;
+
        /*
         * If this is the first possibly-multixact-able operation in the
         * current transaction, set my per-backend OldestMemberMXactId
@@ -5962,7 +5976,9 @@ heap_lock_updated_tuple(Relation rel, HeapTuple tuple, ItemPointer ctid,
         */
        MultiXactIdSetOldestMember();
 
-       return heap_lock_updated_tuple_rec(rel, ctid, xid, mode);
+       prior_xmax = (prior_infomask & HEAP_XMAX_IS_MULTI) ?
+           MultiXactIdGetUpdateXid(prior_raw_xmax, prior_infomask) : prior_raw_xmax;
+       return heap_lock_updated_tuple_rec(rel, prior_xmax, prior_ctid, xid, mode);
    }
 
    /* nothing to lock */