Re: Fix resource leak in FindConflictTuple() retry path

From: shveta malik <shveta(dot)malik(at)gmail(dot)com>
To: Nisha Moond <nisha(dot)moond412(at)gmail(dot)com>
Cc: PostgreSQL Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>, Amit Kapila <amit(dot)kapila16(at)gmail(dot)com>, "Zhijie Hou (Fujitsu)" <houzj(dot)fnst(at)fujitsu(dot)com>, shveta malik <shveta(dot)malik(at)gmail(dot)com>
Subject: Re: Fix resource leak in FindConflictTuple() retry path
Date: 2026-09-03 05:24:22
Message-ID: CAJpy0uDfXFOmP-4cunN04+LiAYx_KSHa7e+-zJ1sxVaDByjJ8w@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On Thu, Sep 3, 2026 at 9:22 AM Nisha Moond <nisha(dot)moond412(at)gmail(dot)com> wrote:
>
> 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
>

I agree with the idea of patch. But the patch can be improved. Before
this patch, slot creation happened after the conflict was found. Now
it happens unconditionally. So every call to FindConflictTuple() now
allocates a slot and immediately tears it down. So most of the cases
which are ocnflict-free now will do slot-allocation. I feel this can
be optimized.

Suggestion:

retry:
if (ExecCheckIndexConstraints(...))
{
if (*conflictslot)
ExecDropSingleTupleTableSlot(*conflictslot);
*conflictslot = NULL;
return false;
}

if (*conflictslot == NULL)
*conflictslot = table_slot_create(rel, NULL);

thanks
Shveta

In response to

Responses

Browse pgsql-hackers by date

  From Date Subject
Next Message Etsuro Fujita 2026-09-03 05:35:53 Re: Remove fcinfo from statistics update internal functions
Previous Message Michael Paquier 2026-09-03 05:15:30 Re: Report index currently being vacuumed in pg_stat_progress_vacuum