From 7b1ffadf79371ff4dbb4462d4bd21ac5a38e4c93 Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Tue, 22 Sep 2026 20:16:12 +0900 Subject: [PATCH] Route RI fast-path permission check through ExecCheckPermissions() ri_CheckPermissions() called ExecCheckOneRelPerms() directly, so ExecutorCheckPerms_hook never saw the referenced-table access that the SPI path's SELECT ... FOR KEY SHARE exposed to it via ExecutorStart(). Modules such as sepgsql that rely on this hook therefore lost control over FK checks against non-partitioned referenced tables. So, build a one-entry range table and permission-info list and call ExecCheckPermissions(). The hook now sees the same relation and requiredPerms as before, once per check. Reported-by: Noah Misch Discussion: https://postgr.es/m/20260705210533.ee.noahmisch%40microsoft.com Backpatch-through: 19 --- src/backend/utils/adt/ri_triggers.c | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 059dafce836..e9edc7cd5e9 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -3613,14 +3613,18 @@ ri_check_fastpath_index(RI_ConstraintInfo *riinfo, * ri_CheckPermissions * Check permissions for the SELECT ... FOR KEY SHARE used by the SPI * path, as the referenced table's owner. + * + * Go through ExecCheckPermissions() with a manufactured range table so that + * ExecutorCheckPerms_hook gets control exactly as it did when the check ran + * through the executor. */ static void ri_CheckPermissions(const RI_ConstraintInfo *riinfo, Relation query_rel) { AclResult aclresult; AclMode requiredPerms = ACL_SELECT | ACL_SELECT_FOR_UPDATE; + RangeTblEntry *rte; RTEPermissionInfo *perminfo; - bool result; /* USAGE on schema. */ aclresult = object_aclcheck(NamespaceRelationId, @@ -3630,16 +3634,10 @@ ri_CheckPermissions(const RI_ConstraintInfo *riinfo, Relation query_rel) aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(RelationGetNamespace(query_rel))); - /* Avoid building the column bitmap when table privileges suffice. */ - if (pg_class_aclmask(RelationGetRelid(query_rel), GetUserId(), - requiredPerms, ACLMASK_ALL) == requiredPerms) - return; - /* * SELECT is needed only on the referenced key columns. FOR KEY SHARE - * also needs UPDATE privilege, which may be granted on any column. Use - * the executor's checks for both, leaving updatedCols empty as the SPI - * query does. + * also needs UPDATE privilege, which may be granted on any column; leave + * updatedCols empty as the SPI query does. */ perminfo = makeNode(RTEPermissionInfo); perminfo->relid = RelationGetRelid(query_rel); @@ -3651,12 +3649,14 @@ ri_CheckPermissions(const RI_ConstraintInfo *riinfo, Relation query_rel) perminfo->selectedCols = bms_add_member(perminfo->selectedCols, attno); } - result = ExecCheckOneRelPerms(perminfo); - bms_free(perminfo->selectedCols); - pfree(perminfo); - if (!result) - aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_TABLE, - RelationGetRelationName(query_rel)); + rte = makeNode(RangeTblEntry); + rte->rtekind = RTE_RELATION; + rte->relid = RelationGetRelid(query_rel); + rte->relkind = query_rel->rd_rel->relkind; + rte->rellockmode = RowShareLock; + rte->perminfoindex = 1; + + (void) ExecCheckPermissions(list_make1(rte), list_make1(perminfo), true); } /* -- 2.47.3