Fix resource leak in FindConflictTuple() retry path

From: Nisha Moond <nisha(dot)moond412(at)gmail(dot)com>
To: PostgreSQL Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>
Cc: Amit Kapila <amit(dot)kapila16(at)gmail(dot)com>, "Zhijie Hou (Fujitsu)" <houzj(dot)fnst(at)fujitsu(dot)com>
Subject: Fix resource leak in FindConflictTuple() retry path
Date: 2026-09-03 03:52:01
Message-ID: CABdArM7xjMova7EU1XT_RQbuTN8A28=eoTmQ_2uicSvOgLBq2Q@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi,

As part of the AI-assisted review of the update_deleted conflict
detection work [1], a small resource leak was identified in
FindConflictTuple(), introduced by commit 9758174e2e5.

When should_refetch_tuple() returns true i.e. when the conflicting
tuple was modified between ExecCheckIndexConstraints() and
table_tuple_lock(), the function retries from its retry label. On each
pass it creates a new slot and assigns it to *conflictslot,
overwriting the previous slot without releasing it. So every retry
abandons one slot.

The abandoned slot also holds a buffer pin. Since FindConflictTuple()
does not pass TUPLE_LOCK_FLAG_FIND_LAST_VERSION, heapam_tuple_lock()
falls through to ExecStorePinnedBufferHeapTuple(), which transfers the
pin to the slot even for TM_Updated. The heap_lock_tuple() failure
path releases the content lock but not the pin. Since the slot has a
NULL reglist, it is not registered in estate->es_tupleTable and is
therefore not cleaned up by ExecResetTupleTable().

This is mostly harmless in practice: the slot is freed with its memory
context, and buffer pins are released at transaction end. The usual
conflict path also aborts the transaction with ERROR, releasing
everything.

Still, a pinned buffer cannot be evicted, and repeated retries on a
contended unique key can accumulate pins for the lifetime of the
transaction, so this seems worth fixing.

The attached patch-001 creates the slot once before the retry loop and
reuses it. Re-storing a tuple in the same slot releases its previous
buffer pin, so no slot is abandoned.

Reproducing the issue:
The window is very narrow and cannot be triggered directly from SQL,
as the concurrent UPDATE must occur between
ExecCheckIndexConstraints() and table_tuple_lock() inside
FindConflictTuple().

I created a small, hacky TAP test (patch-002) with the help of Claude,
which uses an injection point and elog() to reproduce the issue and
verify slot reuse. This test is only for demonstrating the problem and
is not intended for commit.

Since this is an oversight in commit 9758174e2e5, it should be
backpatched to PG18.

Feedback on the fix and approach is welcome.

[1] https://www.postgresql.org/message-id/TY4PR01MB177182F547A62FC2666EC04EC94B72%40TY4PR01MB17718.jpnprd01.prod.outlook.com

--
Thanks,
Nisha

Attachment Content-Type Size
v1-0001-Avoid-re-creating-the-conflict-slot-on-retry-in-F.patch application/octet-stream 2.0 KB
v1-0002-TAP-test-for-FindConflictTuple-buffer-pin-leak.patch application/octet-stream 11.8 KB

Responses

Browse pgsql-hackers by date

  From Date Subject
Next Message Yuya Shinde 2026-09-03 04:05:58 Re: [BUG] Incorrect historic snapshot may be serialized to disk during fast-forwarding
Previous Message Zhijie Hou (Fujitsu) 2026-09-03 03:42:44 Remove stale XXX comment in logical launcher