postgres.git / summary / log / commit / refs
commit 1974acf23cb2a5273821162abe68cd2f70fde682
Author: Nathan Bossart <nathan@postgresql.org>
Commit: Noah Misch <noah@leadboat.com>
Date: Mon Aug 10 13:38:19 2026 +0000
First released in: 14.24, 15.19, 16.15, 17.11, 18.6
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 | 60 ++++++++++++++++++++++++++++++++++++-
src/include/utils/acl.h | 3 ++
3 files changed, 63 insertions(+), 2 deletions(-)
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 6c723e0dbcc..cd18f31bb97 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -77,7 +77,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 5af1a168ec2..2b94910307e 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
@@ -66,6 +69,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"
@@ -116,6 +120,7 @@ static bool ScanQueryWalker(Node *node, bool *acquire);
static TupleDesc PlanCacheComputeResultDesc(List *stmt_list);
static void PlanCacheRelCallback(Datum arg, Oid relid);
static void PlanCacheObjectCallback(Datum arg, int cacheid, uint32 hashvalue);
+static void PlanCacheRoleCallback(Datum arg, int cacheid, uint32 hashvalue);
static void PlanCacheSysCallback(Datum arg, int cacheid, uint32 hashvalue);
/* ResourceOwner callbacks to track plancache references */
@@ -162,6 +167,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);
}
/*
@@ -2168,6 +2176,56 @@ PlanCacheObjectCallback(Datum arg, int 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, int 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 731d84b2a93..96c906bab20 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -227,6 +227,9 @@ extern void select_best_grantor(Oid roleId, 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: 67135960559d]