Re: BUG #17462: Invalid memory access in heapam_tuple_lock - Mailing list pgsql-bugs

From Tom Lane
Subject Re: BUG #17462: Invalid memory access in heapam_tuple_lock
Date
Msg-id 995052.1649799605@sss.pgh.pa.us
Whole thread Raw
In response to Re: BUG #17462: Invalid memory access in heapam_tuple_lock  (Tom Lane <tgl@sss.pgh.pa.us>)
List pgsql-bugs
I wrote:
> As written here, even though there's no obvious API break, there is
> a subtle change: heap_fetch will now return tuple->t_data = NULL after
> a snapshot failure.  I'm of two minds whether to back-patch that part
> (not doing so would require more changes in heapam_tuple_lock, though).

After further thought I think we shouldn't back-patch that part: it
could easily break code that's functionally correct today, and there
isn't any case that it makes better without caller-code changes.

(I'm not sure what made me think it'd affect heapam_tuple_lock; that
function only cares about the behavior with keep_buf = true.)

Hence, I end up with two different patches for HEAD and the back branches.
In HEAD, we explicitly break heap_fetch compatibility by adding a new
argument, and we can make the commit message note the more subtle change
too.  In the back branches, the behavior of heap_fetch is unchanged
and heapam_tuple_lock has to use heap_fetch_extended.

            regards, tom lane

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index ba11bcd99e..f6cca1cb1d 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -1530,10 +1530,14 @@ heap_getnextslot_tidrange(TableScanDesc sscan, ScanDirection direction,
  * must unpin the buffer when done with the tuple.
  *
  * If the tuple is not found (ie, item number references a deleted slot),
- * then tuple->t_data is set to NULL and false is returned.
+ * then tuple->t_data is set to NULL, *userbuf is set to InvalidBuffer,
+ * and false is returned.
  *
- * If the tuple is found but fails the time qual check, then false is returned
- * but tuple->t_data is left pointing to the tuple.
+ * If the tuple is found but fails the time qual check, then the behavior
+ * depends on the keep_buf parameter.  If keep_buf is false, the results
+ * are the same as for the tuple-not-found case.  If keep_buf is true,
+ * then tuple->t_data and *userbuf are returned as for the success case,
+ * and again the caller must unpin the buffer; but false is returned.
  *
  * heap_fetch does not follow HOT chains: only the exact TID requested will
  * be fetched.
@@ -1551,7 +1555,8 @@ bool
 heap_fetch(Relation relation,
            Snapshot snapshot,
            HeapTuple tuple,
-           Buffer *userbuf)
+           Buffer *userbuf,
+           bool keep_buf)
 {
     ItemPointer tid = &(tuple->t_self);
     ItemId        lp;
@@ -1634,9 +1639,15 @@ heap_fetch(Relation relation,
         return true;
     }

-    /* Tuple failed time qual */
-    ReleaseBuffer(buffer);
-    *userbuf = InvalidBuffer;
+    /* Tuple failed time qual, but maybe caller wants to see it anyway. */
+    if (keep_buf)
+        *userbuf = buffer;
+    else
+    {
+        ReleaseBuffer(buffer);
+        *userbuf = InvalidBuffer;
+        tuple->t_data = NULL;
+    }

     return false;
 }
@@ -1659,8 +1670,7 @@ heap_fetch(Relation relation,
  * are vacuumable, false if not.
  *
  * Unlike heap_fetch, the caller must already have pin and (at least) share
- * lock on the buffer; it is still pinned/locked at exit.  Also unlike
- * heap_fetch, we do not report any pgstats count; caller may do so if wanted.
+ * lock on the buffer; it is still pinned/locked at exit.
  */
 bool
 heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
@@ -5379,7 +5389,7 @@ heap_lock_updated_tuple_rec(Relation rel, ItemPointer tid, TransactionId xid,
         block = ItemPointerGetBlockNumber(&tupid);
         ItemPointerCopy(&tupid, &(mytup.t_self));

-        if (!heap_fetch(rel, SnapshotAny, &mytup, &buf))
+        if (!heap_fetch(rel, SnapshotAny, &mytup, &buf, false))
         {
             /*
              * if we fail to find the updated version of the tuple, it's
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 666b6205a7..444f027149 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -188,7 +188,7 @@ heapam_fetch_row_version(Relation relation,
     Assert(TTS_IS_BUFFERTUPLE(slot));

     bslot->base.tupdata.t_self = *tid;
-    if (heap_fetch(relation, snapshot, &bslot->base.tupdata, &buffer))
+    if (heap_fetch(relation, snapshot, &bslot->base.tupdata, &buffer, false))
     {
         /* store in slot, transferring existing pin */
         ExecStorePinnedBufferHeapTuple(&bslot->base.tupdata, slot, buffer);
@@ -401,7 +401,7 @@ tuple_lock_retry:
                              errmsg("tuple to be locked was already moved to another partition due to concurrent
update")));

                 tuple->t_self = *tid;
-                if (heap_fetch(relation, &SnapshotDirty, tuple, &buffer))
+                if (heap_fetch(relation, &SnapshotDirty, tuple, &buffer, true))
                 {
                     /*
                      * If xmin isn't what we're expecting, the slot must have
@@ -500,6 +500,7 @@ tuple_lock_retry:
                  */
                 if (tuple->t_data == NULL)
                 {
+                    Assert(!BufferIsValid(buffer));
                     return TM_Deleted;
                 }

@@ -509,8 +510,7 @@ tuple_lock_retry:
                 if (!TransactionIdEquals(HeapTupleHeaderGetXmin(tuple->t_data),
                                          priorXmax))
                 {
-                    if (BufferIsValid(buffer))
-                        ReleaseBuffer(buffer);
+                    ReleaseBuffer(buffer);
                     return TM_Deleted;
                 }

@@ -526,13 +526,12 @@ tuple_lock_retry:
                  *
                  * As above, it should be safe to examine xmax and t_ctid
                  * without the buffer content lock, because they can't be
-                 * changing.
+                 * changing.  We'd better hold a buffer pin though.
                  */
                 if (ItemPointerEquals(&tuple->t_self, &tuple->t_data->t_ctid))
                 {
                     /* deleted, so forget about it */
-                    if (BufferIsValid(buffer))
-                        ReleaseBuffer(buffer);
+                    ReleaseBuffer(buffer);
                     return TM_Deleted;
                 }

@@ -540,8 +539,7 @@ tuple_lock_retry:
                 *tid = tuple->t_data->t_ctid;
                 /* updated row should have xmin matching this xmax */
                 priorXmax = HeapTupleHeaderGetUpdateXid(tuple->t_data);
-                if (BufferIsValid(buffer))
-                    ReleaseBuffer(buffer);
+                ReleaseBuffer(buffer);
                 /* loop back to fetch next in chain */
             }
         }
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 4403f01e13..5d7f7fd800 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -133,7 +133,7 @@ extern bool heap_getnextslot_tidrange(TableScanDesc sscan,
                                       ScanDirection direction,
                                       TupleTableSlot *slot);
 extern bool heap_fetch(Relation relation, Snapshot snapshot,
-                       HeapTuple tuple, Buffer *userbuf);
+                       HeapTuple tuple, Buffer *userbuf, bool keep_buf);
 extern bool heap_hot_search_buffer(ItemPointer tid, Relation relation,
                                    Buffer buffer, Snapshot snapshot, HeapTuple heapTuple,
                                    bool *all_dead, bool first_call);
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 763dc07361..64b9ec0376 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -1576,10 +1576,13 @@ heap_getnextslot_tidrange(TableScanDesc sscan, ScanDirection direction,
  * must unpin the buffer when done with the tuple.
  *
  * If the tuple is not found (ie, item number references a deleted slot),
- * then tuple->t_data is set to NULL and false is returned.
+ * then tuple->t_data is set to NULL, *userbuf is set to InvalidBuffer,
+ * and false is returned.
  *
  * If the tuple is found but fails the time qual check, then false is returned
- * but tuple->t_data is left pointing to the tuple.
+ * and *userbuf is set to InvalidBuffer, but tuple->t_data is left pointing
+ * to the tuple.  (Note that it is unsafe to dereference tuple->t_data in
+ * this case, but callers might choose to test it for NULL-ness.)
  *
  * heap_fetch does not follow HOT chains: only the exact TID requested will
  * be fetched.
@@ -1598,6 +1601,25 @@ heap_fetch(Relation relation,
            Snapshot snapshot,
            HeapTuple tuple,
            Buffer *userbuf)
+{
+    return heap_fetch_extended(relation, snapshot, tuple, userbuf, false);
+}
+
+/*
+ *    heap_fetch_extended        - fetch tuple even if it fails snapshot test
+ *
+ * If keep_buf is true, then upon finding a tuple that is valid but fails
+ * the snapshot check, we return the tuple pointer in tuple->t_data and the
+ * buffer ID in *userbuf, keeping the buffer pin, just as if it had passed
+ * the snapshot.  (The function result is still "false" though.)
+ * If keep_buf is false then this behaves identically to heap_fetch().
+ */
+bool
+heap_fetch_extended(Relation relation,
+                    Snapshot snapshot,
+                    HeapTuple tuple,
+                    Buffer *userbuf,
+                    bool keep_buf)
 {
     ItemPointer tid = &(tuple->t_self);
     ItemId        lp;
@@ -1680,9 +1702,14 @@ heap_fetch(Relation relation,
         return true;
     }

-    /* Tuple failed time qual */
-    ReleaseBuffer(buffer);
-    *userbuf = InvalidBuffer;
+    /* Tuple failed time qual, but maybe caller wants to see it anyway. */
+    if (keep_buf)
+        *userbuf = buffer;
+    else
+    {
+        ReleaseBuffer(buffer);
+        *userbuf = InvalidBuffer;
+    }

     return false;
 }
@@ -1705,8 +1732,7 @@ heap_fetch(Relation relation,
  * are vacuumable, false if not.
  *
  * Unlike heap_fetch, the caller must already have pin and (at least) share
- * lock on the buffer; it is still pinned/locked at exit.  Also unlike
- * heap_fetch, we do not report any pgstats count; caller may do so if wanted.
+ * lock on the buffer; it is still pinned/locked at exit.
  */
 bool
 heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index d1192e6a0c..66339392d7 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -401,7 +401,8 @@ tuple_lock_retry:
                              errmsg("tuple to be locked was already moved to another partition due to concurrent
update")));

                 tuple->t_self = *tid;
-                if (heap_fetch(relation, &SnapshotDirty, tuple, &buffer))
+                if (heap_fetch_extended(relation, &SnapshotDirty, tuple,
+                                        &buffer, true))
                 {
                     /*
                      * If xmin isn't what we're expecting, the slot must have
@@ -500,6 +501,7 @@ tuple_lock_retry:
                  */
                 if (tuple->t_data == NULL)
                 {
+                    Assert(!BufferIsValid(buffer));
                     return TM_Deleted;
                 }

@@ -509,8 +511,7 @@ tuple_lock_retry:
                 if (!TransactionIdEquals(HeapTupleHeaderGetXmin(tuple->t_data),
                                          priorXmax))
                 {
-                    if (BufferIsValid(buffer))
-                        ReleaseBuffer(buffer);
+                    ReleaseBuffer(buffer);
                     return TM_Deleted;
                 }

@@ -526,13 +527,12 @@ tuple_lock_retry:
                  *
                  * As above, it should be safe to examine xmax and t_ctid
                  * without the buffer content lock, because they can't be
-                 * changing.
+                 * changing.  We'd better hold a buffer pin though.
                  */
                 if (ItemPointerEquals(&tuple->t_self, &tuple->t_data->t_ctid))
                 {
                     /* deleted, so forget about it */
-                    if (BufferIsValid(buffer))
-                        ReleaseBuffer(buffer);
+                    ReleaseBuffer(buffer);
                     return TM_Deleted;
                 }

@@ -540,8 +540,7 @@ tuple_lock_retry:
                 *tid = tuple->t_data->t_ctid;
                 /* updated row should have xmin matching this xmax */
                 priorXmax = HeapTupleHeaderGetUpdateXid(tuple->t_data);
-                if (BufferIsValid(buffer))
-                    ReleaseBuffer(buffer);
+                ReleaseBuffer(buffer);
                 /* loop back to fetch next in chain */
             }
         }
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index e63b49fc38..4f1dff9ca1 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -134,6 +134,9 @@ extern bool heap_getnextslot_tidrange(TableScanDesc sscan,
                                       TupleTableSlot *slot);
 extern bool heap_fetch(Relation relation, Snapshot snapshot,
                        HeapTuple tuple, Buffer *userbuf);
+extern bool heap_fetch_extended(Relation relation, Snapshot snapshot,
+                                HeapTuple tuple, Buffer *userbuf,
+                                bool keep_buf);
 extern bool heap_hot_search_buffer(ItemPointer tid, Relation relation,
                                    Buffer buffer, Snapshot snapshot, HeapTuple heapTuple,
                                    bool *all_dead, bool first_call);

pgsql-bugs by date:

Previous
From: Tom Lane
Date:
Subject: Re: BUG #17464: Domain type. If the value field(CREATE DOMAIN) is set to null, errors cannot be intercepted.
Next
From: Susanne Holzgraefe
Date:
Subject: replace() using NULL