Hi Hackers, Here is a patch to fix some memory leaks in BEFORE/INSTEAD OF triggers.
Andres reported the problem in [0] while reviewing FOR PORTION OF, but as he suspected, it is a problem on master too. It goes back to v14 at least. You can reach it if (1) the rel is not a regular table (e.g. a view) and (2) the trigger returns NEW without changing it (or NULL in ExecIRInsertTriggers). [0] https://www.postgresql.org/message-id/yfers7d2kizq6ockz4nu7e7avq3gj5anpd7ilxkazyvicv4qgy@3cesow2bbhnx Yours, -- Paul ~{:-) [email protected]
From 6e1fd517906f9a736b794318ba361f5b2387622c Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" <[email protected]> Date: Sat, 12 Sep 2026 10:51:49 -0700 Subject: [PATCH v1] Free the tuple fetched for BEFORE/INSTEAD OF row triggers We were leaking the HeapTuple fetched by ExecBRInsertTriggers, ExecBRUpdateTriggers, ExecIRInsertTriggers, and ExecIRUpdateTriggers if both (1) the rel was not an ordinary table (forcing us to use a virtual slot instead of a heap slot) and (2) the trigger returned NEW unchanged (or NULL in ExecIRInsertTriggers). Reported-by: Andres Freund <[email protected]> Author: Paul A. Jungwirth <[email protected]> Backpatch-through: 14 --- src/backend/commands/trigger.c | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 46ba932383f..ce2088c9cc1 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -2508,7 +2508,7 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo, { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; HeapTuple newtuple = NULL; - bool should_free; + bool should_free = false; TriggerData LocTriggerData = {0}; int i; @@ -2577,6 +2577,10 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo, } } + /* Free our copy of the tuple, if the last trigger handed it back. */ + if (newtuple != NULL && should_free) + heap_freetuple(newtuple); + return true; } @@ -2612,7 +2616,7 @@ ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo, { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; HeapTuple newtuple = NULL; - bool should_free; + bool should_free = false; TriggerData LocTriggerData = {0}; int i; @@ -2664,6 +2668,10 @@ ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo, } } + /* Free our copy of the tuple, if the last trigger handed it back. */ + if (newtuple != NULL && should_free) + heap_freetuple(newtuple); + return true; } @@ -3165,6 +3173,10 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, newtuple = NULL; } } + + /* Likewise free our copy of the new tuple, if we still have it. */ + if (newtuple != NULL && should_free_new) + heap_freetuple(newtuple); if (should_free_trig) heap_freetuple(trigtuple); @@ -3258,7 +3270,7 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo, TriggerDesc *trigdesc = relinfo->ri_TrigDesc; TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo); HeapTuple newtuple = NULL; - bool should_free; + bool should_free = false; TriggerData LocTriggerData = {0}; int i; @@ -3300,6 +3312,8 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo, GetPerTupleMemoryContext(estate)); if (newtuple == NULL) { + if (should_free) + heap_freetuple(oldtuple); return false; /* "do nothing" */ } else if (newtuple != oldtuple) @@ -3314,6 +3328,10 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo, } } + /* Free our copy of the new tuple, if the last trigger handed it back. */ + if (newtuple != NULL && should_free) + heap_freetuple(newtuple); + return true; } -- 2.47.3
