postgres.git / summary / log / commit / refs

commit    ffca23839ccaaf8aed5e0729cb041b53f981f04c
Author:   Nathan Bossart <nathan@postgresql.org>
Commit:   Noah Misch <noah@leadboat.com>
Date:     Mon Aug 10 13:37:50 2026 +0000

    Invalidate plan cache after role changes.
    
    Role membership, role attribute, and database ownership changes may
    impact the expected behavior of row-level security policies, but
    currently the plan cache doesn't take notice.  To fix, register
    syscache callbacks on pg_auth_members, pg_authid, and pg_database
    that invalidate the role-dependent plans.  Changes to other
    databases' pg_database rows are ignored.
    
    Reported-by: Ilya Staroverov <i.staroverov@ftdata.ru>
    Reported-by: Shinya Kato <shinya11.kato@gmail.com>
    Author: Ilya Staroverov <i.staroverov@ftdata.ru>
    Author: Shinya Kato <shinya11.kato@gmail.com>
    Co-authored-by: Nathan Bossart <nathandbossart@gmail.com>
    Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
    Security: CVE-2026-14666
    Backpatch-through: 14


src/backend/utils/adt/acl.c | 2 +- src/backend/utils/cache/plancache.c | 61 ++++++++++++++++++++++++++++++++++++- src/include/utils/acl.h | 3 ++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index e2547d719ed..e2bfa77f498 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -81,7 +81,7 @@ enum RoleRecurseType }; static Oid cached_role[] = {InvalidOid, InvalidOid, InvalidOid}; static List *cached_roles[] = {NIL, NIL, NIL}; -static uint32 cached_db_hash; +uint32 cached_db_hash; /* * If the list of roles gathered by roles_is_member_of() grows larger than the diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 26f1bd64515..fb3b38ffbbf 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -36,7 +36,10 @@ * certain other system catalogs, such as pg_namespace; but for them, our * response is just to invalidate all plans. We expect updates on those * catalogs to be infrequent enough that more-detailed tracking is not worth - * the effort. + * the effort. We likewise watch pg_authid, pg_auth_members, and + * pg_database, which can change which row-level security policies apply. + * Since those are shared catalogs whose inval events reach every backend + * in the cluster, we invalidate only the role-dependent plans. * * In addition to full-fledged query plans, we provide a facility for * detecting invalidations of simple scalar expressions. This is fairly @@ -67,6 +70,7 @@ #include "storage/lmgr.h" #include "tcop/pquery.h" #include "tcop/utility.h" +#include "utils/acl.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/resowner.h" @@ -108,6 +112,8 @@ static TupleDesc PlanCacheComputeResultDesc(List *stmt_list); static void PlanCacheRelCallback(Datum arg, Oid relid); static void PlanCacheObjectCallback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue); +static void PlanCacheRoleCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); static void PlanCacheSysCallback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue); @@ -155,6 +161,9 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(AUTHMEMROLEMEM, PlanCacheRoleCallback, (Datum) 0); + CacheRegisterSyscacheCallback(AUTHOID, PlanCacheRoleCallback, (Datum) 0); + CacheRegisterSyscacheCallback(DATABASEOID, PlanCacheRoleCallback, (Datum) 0); } /* @@ -2309,6 +2318,56 @@ PlanCacheObjectCallback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) } } +/* + * PlanCacheRoleCallback + * Syscache inval callback function for AUTHMEMROLEMEM, AUTHOID, and + * DATABASEOID caches + * + * Role membership, role attributes, and database ownership (which confers + * membership in pg_database_owner) affect planning by way of row-level + * security, so invalidate just the role-dependent plans. For DATABASEOID, we + * can ignore changes to other databases' pg_database rows. + */ +static void +PlanCacheRoleCallback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) +{ + dlist_iter iter; + + if (cacheid == DATABASEOID && + hashvalue != cached_db_hash && + hashvalue != 0) + return; /* ignore pg_database changes for other DBs */ + + dlist_foreach(iter, &saved_plan_list) + { + CachedPlanSource *plansource = dlist_container(CachedPlanSource, + node, iter.cur); + + Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC); + + /* No work if it's already invalidated */ + if (!plansource->is_valid) + continue; + + /* Never invalidate if parse/plan would be a no-op anyway */ + if (!StmtPlanRequiresRevalidation(plansource)) + continue; + + if (plansource->dependsOnRLS) + { + /* Invalidate the querytree and generic plan */ + plansource->is_valid = false; + if (plansource->gplan) + plansource->gplan->is_valid = false; + } + else if (plansource->gplan && plansource->gplan->dependsOnRole) + { + /* Invalidate the generic plan only */ + plansource->gplan->is_valid = false; + } + } +} + /* * PlanCacheSysCallback * Syscache inval callback function for other caches diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 0b9b04e78ee..a16bf2fa15a 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -228,6 +228,9 @@ extern void select_best_grantor(const RoleSpec *grantedBy, AclMode privileges, const Acl *acl, Oid ownerId, Oid *grantorId, AclMode *grantOptions); +/* DATABASEOID syscache hash value for our own database, set by initialize_acl */ +extern uint32 cached_db_hash; + extern void initialize_acl(void); /* [parent: 53ed5ebd84fb]