From c283389c9ea5767eb6bd52a7a5b5ef6d7186c5a7 Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Fri, 24 Jul 2026 18:48:43 +0900 Subject: [PATCH v2 4/6] 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 constraint is invalidated the FastPathMeta is pfree'd, but 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: 1000 invalidations of a constraint on a 20-column composite key leaked about 450kB. 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. 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 Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/20260705210533.ee.noahmisch@microsoft.com Backpatch-through: 19 --- src/backend/utils/adt/ri_triggers.c | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index f95f72348e1..dc2119cf475 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -159,6 +159,19 @@ typedef struct FastPathMeta Oid subtypes[RI_MAX_NUMKEYS]; 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 and destroyed with this struct, rather than + * TopMemoryContext: otherwise whatever they cached is orphaned when this + * metadata is invalidated and freed. + * + * 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; } FastPathMeta; /* @@ -2595,6 +2608,7 @@ InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid, riinfo->valid = false; if (riinfo->fpmeta) { + MemoryContextDelete(riinfo->fpmeta->scratch_cxt); pfree(riinfo->fpmeta); riinfo->fpmeta = NULL; } @@ -3543,6 +3557,12 @@ ri_populate_fastpath_metadata(RI_ConstraintInfo *riinfo, Assert(riinfo != NULL && riinfo->valid); fpmeta = palloc_object(FastPathMeta); + + /* 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]; @@ -3568,9 +3588,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, -- 2.47.3