From e8dfb066e7d49cc1550969871b43fac9ed01c04c Mon Sep 17 00:00:00 2001 From: luquijeffrey Date: Fri, 29 May 2026 12:23:54 -0300 Subject: [PATCH 2/2] =?UTF-8?q?Fix=20crash=20of=20self=E2=80=91referencing?= =?UTF-8?q?=20tables=20with=20delete=20triggers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/utils/adt/ri_triggers.c | 130 +++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index dc89c686394..425e7f9e1eb 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -251,12 +251,25 @@ typedef struct RI_FastPathEntry int batch_count; } RI_FastPathEntry; +/* + * RI_QueryPlanCacheExecutingRefCountEntry + * + * Entry to track the number of times a prepared plan is being executed. + */ +typedef struct RI_QueryPlanCacheExecutingRefCountEntry +{ + SPIPlanPtr plan; + bool markedForDeletion; /* If true, it will be freed when refcount reaches 0 */ + uint32 refcount; /* number of times this plan is being executed (can be more than 1 if reentrant) */ +} RI_QueryPlanCacheExecutingRefCountEntry; + /* * Local data */ static HTAB *ri_constraint_cache = NULL; static HTAB *ri_query_cache = NULL; static HTAB *ri_compare_cache = NULL; +static HTAB *ri_query_plan_cache_executing_refcount = NULL; static dclist_head ri_constraint_cache_valid_list; static HTAB *ri_fastpath_cache = NULL; @@ -295,6 +308,11 @@ static SPIPlanPtr ri_FetchPreparedPlan(RI_QueryKey *key); static void ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan); static RI_CompareHashEntry *ri_HashCompareOp(Oid eq_opr, Oid typeid); +/* Reentrancy protection: prevent segfault on deleting a plan in execution if invalidated during reentrant RI check. */ +static void ri_PreparedPlanExecutionStarted(SPIPlanPtr plan); +static void ri_PreparedPlanExecutionFinished(SPIPlanPtr plan); +static void ri_PreparedPlanReleaseASAP(SPIPlanPtr plan); + static void ri_CheckTrigger(FunctionCallInfo fcinfo, const char *funcname, int tgkind); static RI_ConstraintInfo *ri_FetchConstraintInfo(Trigger *trigger, @@ -2724,6 +2742,9 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, save_sec_context | SECURITY_LOCAL_USERID_CHANGE | SECURITY_NOFORCE_RLS); + /* Increase plan use count for reentrancy protection. */ + ri_PreparedPlanExecutionStarted(qplan); + /* * Finally we can run the query. * @@ -2735,6 +2756,9 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, vals, nulls, test_snapshot, crosscheck_snapshot, false, false, limit); + + /* Decrease plan use count. this call can free the plan if it was invalidated and no longer in use. */ + ri_PreparedPlanExecutionFinished(qplan); /* Restore UID and security context */ SetUserIdAndSecContext(save_userid, save_sec_context); @@ -3762,6 +3786,12 @@ ri_InitHashTables(void) ri_compare_cache = hash_create("RI compare cache", RI_INIT_QUERYHASHSIZE, &ctl, HASH_ELEM | HASH_BLOBS); + + ctl.keysize = sizeof(SPIPlanPtr); + ctl.entrysize = sizeof(RI_QueryPlanCacheExecutingRefCountEntry); + ri_query_plan_cache_executing_refcount = hash_create("RI plan cache execution refcount", + RI_INIT_QUERYHASHSIZE, + &ctl, HASH_ELEM | HASH_BLOBS); } @@ -3812,7 +3842,7 @@ ri_FetchPreparedPlan(RI_QueryKey *key) */ entry->plan = NULL; if (plan) - SPI_freeplan(plan); + ri_PreparedPlanReleaseASAP(plan); return NULL; } @@ -3847,6 +3877,104 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan) } +static void +ri_PreparedPlanExecutionStarted(SPIPlanPtr plan) +{ + RI_QueryPlanCacheExecutingRefCountEntry* entry; + bool found; + + if (!ri_query_plan_cache_executing_refcount) + ri_InitHashTables(); + + entry = (RI_QueryPlanCacheExecutingRefCountEntry*) hash_search(ri_query_plan_cache_executing_refcount, &plan, HASH_ENTER, &found); + if (found) + entry->refcount++; + else + { + entry->refcount = 1; + entry->markedForDeletion = false; + } +} + +static void +ri_PreparedPlanExecutionFinished(SPIPlanPtr plan) +{ + RI_QueryPlanCacheExecutingRefCountEntry* entry; + bool found; + + if (!ri_query_plan_cache_executing_refcount) + return; + + entry = (RI_QueryPlanCacheExecutingRefCountEntry*) hash_search(ri_query_plan_cache_executing_refcount, &plan, HASH_FIND, &found); + if (!entry) + return; + + entry->refcount--; + if (entry->refcount == 0 && entry->markedForDeletion) + { + // Remove the entry + hash_search(ri_query_plan_cache_executing_refcount, &plan, HASH_REMOVE, NULL); + SPI_freeplan(plan); + } +} + +/* + * ri_PreparedPlanReleaseASAP + * + * Release a cached SPI plan, or mark it for deferred deletion if it + * is currently in use. + * + * If the plan has an active executing-refcount entry with refcount > 0, + * we cannot free it immediately. Instead we mark it for deletion so + * that the last executor to finish will free it. + */ +static void +ri_PreparedPlanReleaseASAP(SPIPlanPtr plan) +{ + RI_QueryPlanCacheExecutingRefCountEntry *entry; + bool found; + + /* + * If there is no executing-refcount hash table, it's not in use, + * so we can free immediately. + */ + if (!ri_query_plan_cache_executing_refcount) + { + SPI_freeplan(plan); + return; + } + + entry = (RI_QueryPlanCacheExecutingRefCountEntry *) + hash_search(ri_query_plan_cache_executing_refcount, + &plan, HASH_FIND, &found); + + /* + * No refcount entry means the plan is not being executed; free it now. + */ + if (!found) + { + SPI_freeplan(plan); + return; + } + + /* + * If the refcount has dropped to zero, remove the entry and free the + * plan. Otherwise mark it for deletion once the last executor finishes. + */ + if (entry->refcount == 0) + { + hash_search(ri_query_plan_cache_executing_refcount, + &plan, HASH_REMOVE, NULL); + SPI_freeplan(plan); + return; + } + + /* + * Mark for deletion once the last executor finishes. + */ + entry->markedForDeletion = true; +} + /* * ri_KeysEqual - * -- 2.34.1