From 626e9e845560373bbaa98e96ada8d88372bcba76 Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Fri, 7 Aug 2026 21:54:03 +0900
Subject: [PATCH v1 2/2] Give RI fast-path cached FmgrInfos their own memory
 context

ri_populate_fastpath_metadata() copies the cast and equality FmgrInfos
into the cached FastPathMeta with fn_mcxt set to TopMemoryContext, the
context active at the copy.  fn_mcxt is scratch space for the called
function: record_eq(), and the record I/O functions generally, allocate
their per-call cache there and keep a pointer to it in fn_extra.

Because that scratch is in TopMemoryContext, it outlives the metadata.
When the metadata is discarded on invalidation, whatever the cast and
equality functions cached is left behind, with nothing pointing at it.
Each subsequent repopulation allocates afresh, so a session that
repeatedly invalidates a foreign key constraint grows TopMemoryContext
without bound.

Give the metadata its own context for the FmgrInfos' fn_mcxt and delete
it along with the metadata, so the cached scratch is freed with the
FmgrInfos that point at it.  Since the preceding commit defers release
of the metadata to AtEOXact_RI(), the context is deleted there rather
than in InvalidateConstraintCacheCallBack().

The context deliberately is not reset while the metadata is in use.
fn_extra points into fn_mcxt, so resetting it would leave those
pointers dangling; the next call would find fn_extra non-NULL and read
freed memory.  fmgr_info_copy() zeroing fn_extra in the copy is the
same invariant seen from the other side.  Nothing accumulates in the
context during use in any case: record_eq() and friends allocate only
when fn_extra is NULL and reuse the cache afterwards.

Reported-by: Noah Misch <noah@leadboat.com>
Reviewed-by: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Discussion: https://postgr.es/m/20260705210533.ee.noahmisch@microsoft.com
Discussion: https://postgr.es/m/CA+HiwqFFB6vzx8v3t2=rbNYyxMistLf5kkJfqzJ81nadFyLrxA@mail.gmail.com
Backpatch-through: 19
---
 src/backend/utils/adt/ri_triggers.c | 22 ++++++++++++++++++++--
 1 file changed, 20 insertions(+), 2 deletions(-)

diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index bce3b6775ee..960624edce0 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -160,6 +160,18 @@ typedef struct FastPathMeta
 	int			strats[RI_MAX_NUMKEYS];
 	AttrNumber	index_attnos[RI_MAX_NUMKEYS];	/* index column positions */
 
+	/*
+	 * fn_mcxt for the cached FmgrInfos above.  Cast and equality functions
+	 * (e.g. record_eq()) use fn_mcxt as scratch space, caching state there and
+	 * keeping a pointer to it in FmgrInfo.fn_extra.  Give them a context of
+	 * their own, created with this struct and destroyed with it in
+	 * AtEOXact_RI().
+	 *
+	 * Note this context must not be reset while the FmgrInfos remain in use,
+	 * since that would free the state fn_extra still points at.
+	 */
+	MemoryContext scratch_cxt;
+
 	/* Link in ri_fpmeta_dead_list while awaiting deferred release */
 	struct FastPathMeta *next_dead;
 } FastPathMeta;
@@ -3584,6 +3596,11 @@ ri_populate_fastpath_metadata(RI_ConstraintInfo *riinfo,
 
 	fpmeta = palloc_object(FastPathMeta);
 	fpmeta->next_dead = NULL;
+
+	/* Scratch context for the cached FmgrInfos' fn_mcxt; see FastPathMeta. */
+	fpmeta->scratch_cxt = AllocSetContextCreate(TopMemoryContext,
+												"RI fast-path finfo scratch",
+												ALLOCSET_SMALL_SIZES);
 	for (int i = 0; i < riinfo->nkeys; i++)
 	{
 		Oid			eq_opr = riinfo->pf_eq_oprs[i];
@@ -3609,9 +3626,9 @@ ri_populate_fastpath_metadata(RI_ConstraintInfo *riinfo,
 		fpmeta->index_attnos[i] = idx_col + 1;
 
 		fmgr_info_copy(&fpmeta->cast_func_finfo[i], &entry->cast_func_finfo,
-					   CurrentMemoryContext);
+					   fpmeta->scratch_cxt);
 		fmgr_info_copy(&fpmeta->eq_opr_finfo[i], &entry->eq_opr_finfo,
-					   CurrentMemoryContext);
+					   fpmeta->scratch_cxt);
 		fpmeta->regops[i] = get_opcode(eq_opr);
 
 		get_op_opfamily_properties(eq_opr,
@@ -4409,6 +4426,7 @@ AtEOXact_RI(bool isCommit)
 		FastPathMeta *dead = ri_fpmeta_dead_list;
 
 		ri_fpmeta_dead_list = dead->next_dead;
+		MemoryContextDelete(dead->scratch_cxt);
 		pfree(dead);
 	}
 }
-- 
2.47.3

