From 6e1fd517906f9a736b794318ba361f5b2387622c Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
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 <andres@anarazel.de>
Author: Paul A. Jungwirth <pj@illuminatedcomputing.com>
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

